@smooai/chat-widget 0.10.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smooai/chat-widget",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "description": "Embeddable AI chat as a framework-light web component — the Aurora Glass design, streaming replies, grounded sources, and per-brand theming. Speaks the smooth-operator WebSocket protocol.",
5
5
  "license": "MIT",
6
6
  "author": "SmooAI",
package/src/config.ts CHANGED
@@ -106,6 +106,12 @@ export interface ChatWidgetConfig {
106
106
  * Clicking one sends it. Capped at 5 for layout.
107
107
  */
108
108
  examplePrompts?: string[];
109
+ /**
110
+ * Show mid-conversation suggested-reply chips ("quick replies") under the
111
+ * latest assistant message when the agent returns follow-up suggestions.
112
+ * Clicking one sends it. Defaults to `true` (shown unless explicitly `false`).
113
+ */
114
+ showSuggestedReplies?: boolean;
109
115
  /** Require the visitor's name before chatting. */
110
116
  requireName?: boolean;
111
117
  /** Require the visitor's email before chatting. */
@@ -180,6 +186,7 @@ export function resolveConfig(config: ChatWidgetConfig): ResolvedConfig {
180
186
  startOpen: config.startOpen ?? false,
181
187
  hideBranding: config.hideBranding ?? false,
182
188
  examplePrompts: (config.examplePrompts ?? []).filter((p) => p.trim().length > 0).slice(0, 5),
189
+ showSuggestedReplies: config.showSuggestedReplies ?? true,
183
190
  requireName: config.requireName ?? false,
184
191
  requireEmail: config.requireEmail ?? false,
185
192
  requirePhone: config.requirePhone ?? false,
@@ -75,6 +75,13 @@ export interface ChatMessage {
75
75
  * defensively off the terminal event — see {@link extractCitations}.
76
76
  */
77
77
  citations?: Citation[];
78
+ /**
79
+ * Suggested follow-up replies ("quick replies") the agent offered on the
80
+ * terminal `eventual_response`. Set ONLY on the finalized assistant message —
81
+ * never mid-stream. Read defensively (see {@link extractSuggestions}); capped
82
+ * at 4 for layout. Absent when the turn offered none.
83
+ */
84
+ suggestions?: string[];
78
85
  }
79
86
 
80
87
  export type ConnectionStatus = 'idle' | 'connecting' | 'ready' | 'error' | 'closed';
@@ -183,6 +190,20 @@ function extractCitations(inner: unknown): Citation[] {
183
190
  return out;
184
191
  }
185
192
 
193
+ /**
194
+ * Pull the suggested follow-up replies out of a terminal `eventual_response`'s
195
+ * `response` object (`response.suggestedNextActions`). Optional + back-compatible
196
+ * like citations — read defensively (tolerating absence, non-array shapes, and
197
+ * non-string / blank entries) and capped at 4 so a chatty agent can't overflow
198
+ * the composer chip row.
199
+ */
200
+ function extractSuggestions(response: unknown): string[] {
201
+ if (!response || typeof response !== 'object') return [];
202
+ const raw = (response as { suggestedNextActions?: unknown }).suggestedNextActions;
203
+ if (!Array.isArray(raw)) return [];
204
+ return raw.filter((s): s is string => typeof s === 'string' && s.trim().length > 0).slice(0, 4);
205
+ }
206
+
186
207
  /** A `get_conversation_messages` row, narrowed defensively off the wire. */
187
208
  interface WireMessage {
188
209
  id?: string;
@@ -623,6 +644,11 @@ export class ConversationController {
623
644
  if (citations.length > 0) {
624
645
  assistant.citations = citations;
625
646
  }
647
+ // Suggested follow-up replies from the terminal event, when present.
648
+ const suggestions = extractSuggestions(inner?.response);
649
+ if (suggestions.length > 0) {
650
+ assistant.suggestions = suggestions;
651
+ }
626
652
  assistant.streaming = false;
627
653
  this.emitMessages();
628
654
  } catch (err) {
package/src/element.ts CHANGED
@@ -97,6 +97,8 @@ export class SmoothAgentChatElement extends HTMLElement {
97
97
  private hasSent = false;
98
98
  /** Starter prompts shown as chips in the empty state. */
99
99
  private examplePrompts: string[] = [];
100
+ /** Whether mid-conversation suggested-reply chips are shown (config). */
101
+ private showSuggestedReplies = true;
100
102
  /** Resolved greeting text, cached so async (rAF) renders can reuse it. */
101
103
  private greeting = '';
102
104
  /** Current mid-turn interrupt (OTP / tool-confirmation), or null. */
@@ -117,6 +119,7 @@ export class SmoothAgentChatElement extends HTMLElement {
117
119
  private dotEl: HTMLElement | null = null;
118
120
  private inputEl: HTMLTextAreaElement | null = null;
119
121
  private sendBtn: HTMLButtonElement | null = null;
122
+ private suggestionsEl: HTMLElement | null = null;
120
123
 
121
124
  // ── Smooth streaming reveal ──
122
125
  // Tokens arrive in variable-size bursts at uneven rates, so revealing text in
@@ -209,6 +212,7 @@ export class SmoothAgentChatElement extends HTMLElement {
209
212
  startOpen: this.overrides.startOpen ?? this.hasAttribute('start-open'),
210
213
  hideBranding: this.overrides.hideBranding ?? this.hasAttribute('hide-branding'),
211
214
  examplePrompts: this.overrides.examplePrompts,
215
+ showSuggestedReplies: this.overrides.showSuggestedReplies,
212
216
  requireName: this.overrides.requireName,
213
217
  requireEmail: this.overrides.requireEmail,
214
218
  requirePhone: this.overrides.requirePhone,
@@ -247,10 +251,14 @@ export class SmoothAgentChatElement extends HTMLElement {
247
251
  onInterrupt: (interrupt) => {
248
252
  this.interrupt = interrupt;
249
253
  this.renderInterrupt();
254
+ // Suggestion chips are hidden while an interrupt overlay is up.
255
+ this.renderSuggestions();
250
256
  },
251
257
  onIdentityRestore: (state) => {
252
258
  this.identityRestore = state;
253
259
  this.renderInterrupt();
260
+ // The restore overlay reuses the interrupt slot — hide chips too.
261
+ this.renderSuggestions();
254
262
  },
255
263
  });
256
264
  if (resolved.startOpen) this.open = true;
@@ -303,6 +311,7 @@ export class SmoothAgentChatElement extends HTMLElement {
303
311
 
304
312
  // Remember starter prompts + greeting for the empty-state chips / async renders.
305
313
  this.examplePrompts = resolved.examplePrompts;
314
+ this.showSuggestedReplies = resolved.showSuggestedReplies;
306
315
  this.greeting = resolved.greeting;
307
316
 
308
317
  // Gate the conversation behind a pre-chat identity form when required.
@@ -348,6 +357,7 @@ export class SmoothAgentChatElement extends HTMLElement {
348
357
  const footerHtml = footerInner ? `<div class="footer">${footerInner}</div>` : '';
349
358
  const chatHtml = `
350
359
  <div class="messages"></div>
360
+ <div class="reply-suggestions"></div>
351
361
  <div class="interrupt hidden"></div>
352
362
  <div class="composer-wrap">
353
363
  <div class="composer">
@@ -385,6 +395,7 @@ export class SmoothAgentChatElement extends HTMLElement {
385
395
  this.inputEl = container.querySelector('textarea');
386
396
  this.sendBtn = container.querySelector('.send');
387
397
  this.interruptEl = container.querySelector('.interrupt');
398
+ this.suggestionsEl = container.querySelector('.reply-suggestions');
388
399
 
389
400
  this.launcherEl?.addEventListener('click', () => this.openChat());
390
401
  container.querySelector('.close')?.addEventListener('click', () => this.closeChat());
@@ -936,6 +947,39 @@ export class SmoothAgentChatElement extends HTMLElement {
936
947
  }
937
948
  }
938
949
  this.scrollToBottom(true);
950
+ this.renderSuggestions();
951
+ }
952
+
953
+ /**
954
+ * Render (or clear) the mid-conversation suggested-reply chips under the
955
+ * composer. Shown ONLY when: the feature is enabled, no interrupt / restore
956
+ * overlay is active (those reuse the slot above the composer), and the LATEST
957
+ * message is a finalized assistant turn that carried suggestions. A new turn
958
+ * (agent or the visitor typing) appends messages, so the latest is no longer
959
+ * that assistant message and the chips clear on the next render. Chips reuse
960
+ * the empty-state `.prompts`/`.chip` styling and send via the same path.
961
+ */
962
+ private renderSuggestions(): void {
963
+ const el = this.suggestionsEl;
964
+ if (!el) return;
965
+ el.replaceChildren();
966
+ if (!this.showSuggestedReplies) return;
967
+ // Hidden while an OTP / tool-confirmation / restore overlay is active.
968
+ if (this.interrupt || this.identityRestore.phase !== 'idle') return;
969
+ const last = this.messages[this.messages.length - 1];
970
+ if (!last || last.role !== 'assistant' || last.streaming || !last.suggestions || last.suggestions.length === 0) return;
971
+
972
+ const chips = document.createElement('div');
973
+ chips.className = 'prompts';
974
+ for (const suggestion of last.suggestions) {
975
+ const chip = document.createElement('button');
976
+ chip.type = 'button';
977
+ chip.className = 'chip';
978
+ chip.textContent = suggestion;
979
+ chip.addEventListener('click', () => this.submitPrompt(suggestion));
980
+ chips.appendChild(chip);
981
+ }
982
+ el.appendChild(chips);
939
983
  }
940
984
 
941
985
  // ─────────────────────── Smooth streaming reveal ───────────────────────────
package/src/styles.ts CHANGED
@@ -605,6 +605,10 @@ ${
605
605
  transform: translateY(-1px);
606
606
  }
607
607
 
608
+ /* ───────────── Mid-conversation suggested-reply chips ─────────────── */
609
+ .reply-suggestions { padding: 0 14px 4px; }
610
+ .reply-suggestions:empty { display: none; }
611
+
608
612
  /* ─────────────── OTP / tool-confirmation interrupt ────────────────── */
609
613
  .interrupt { padding: 0 14px; }
610
614
  .int-card {