@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/README.md +3 -0
- package/dist/chat-widget.global.js +58 -0
- package/dist/chat-widget.global.js.map +1 -1
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +7 -0
- package/src/conversation.ts +26 -0
- package/src/element.ts +44 -0
- package/src/styles.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smooai/chat-widget",
|
|
3
|
-
"version": "0.
|
|
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,
|
package/src/conversation.ts
CHANGED
|
@@ -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 {
|