@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 +4 -0
- package/dist/chat-widget.global.js +72 -5
- package/dist/chat-widget.global.js.map +1 -1
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +72 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +14 -0
- package/src/conversation.ts +26 -0
- package/src/element.ts +64 -4
- package/src/styles.ts +8 -1
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
|
@@ -95,11 +95,23 @@ export interface ChatWidgetConfig {
|
|
|
95
95
|
connectionErrorMessage?: string;
|
|
96
96
|
/** Start the panel open instead of collapsed to the launcher. */
|
|
97
97
|
startOpen?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Hide the "powered by smooth-operator" branding in the header tag and the
|
|
100
|
+
* composer footer. Defaults to `false` (branding shown). The `hide-branding`
|
|
101
|
+
* HTML attribute maps to this.
|
|
102
|
+
*/
|
|
103
|
+
hideBranding?: boolean;
|
|
98
104
|
/**
|
|
99
105
|
* Suggested starter prompts shown as clickable chips before the first message.
|
|
100
106
|
* Clicking one sends it. Capped at 5 for layout.
|
|
101
107
|
*/
|
|
102
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;
|
|
103
115
|
/** Require the visitor's name before chatting. */
|
|
104
116
|
requireName?: boolean;
|
|
105
117
|
/** Require the visitor's email before chatting. */
|
|
@@ -172,7 +184,9 @@ export function resolveConfig(config: ChatWidgetConfig): ResolvedConfig {
|
|
|
172
184
|
greeting: config.greeting ?? 'Hi! How can I help you today?',
|
|
173
185
|
connectionErrorMessage: config.connectionErrorMessage ?? "We couldn't reach the chat. Please try again in a moment.",
|
|
174
186
|
startOpen: config.startOpen ?? false,
|
|
187
|
+
hideBranding: config.hideBranding ?? false,
|
|
175
188
|
examplePrompts: (config.examplePrompts ?? []).filter((p) => p.trim().length > 0).slice(0, 5),
|
|
189
|
+
showSuggestedReplies: config.showSuggestedReplies ?? true,
|
|
176
190
|
requireName: config.requireName ?? false,
|
|
177
191
|
requireEmail: config.requireEmail ?? false,
|
|
178
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
|
@@ -49,7 +49,10 @@ function phoneToE164(value: string): string | null {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
/** Public smooth-operator repo — the "powered by" header tag + footer link here. */
|
|
53
|
+
const SMOOTH_OPERATOR_URL = 'https://github.com/SmooAI/smooth-operator';
|
|
54
|
+
|
|
55
|
+
const OBSERVED = ['endpoint', 'agent-id', 'agent-name', 'logo-url', 'placeholder', 'greeting', 'start-open', 'mode', 'hide-branding'] as const;
|
|
53
56
|
|
|
54
57
|
/**
|
|
55
58
|
* Inline SVG icons (static, trusted strings — never interpolated with user data).
|
|
@@ -94,6 +97,8 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
94
97
|
private hasSent = false;
|
|
95
98
|
/** Starter prompts shown as chips in the empty state. */
|
|
96
99
|
private examplePrompts: string[] = [];
|
|
100
|
+
/** Whether mid-conversation suggested-reply chips are shown (config). */
|
|
101
|
+
private showSuggestedReplies = true;
|
|
97
102
|
/** Resolved greeting text, cached so async (rAF) renders can reuse it. */
|
|
98
103
|
private greeting = '';
|
|
99
104
|
/** Current mid-turn interrupt (OTP / tool-confirmation), or null. */
|
|
@@ -114,6 +119,7 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
114
119
|
private dotEl: HTMLElement | null = null;
|
|
115
120
|
private inputEl: HTMLTextAreaElement | null = null;
|
|
116
121
|
private sendBtn: HTMLButtonElement | null = null;
|
|
122
|
+
private suggestionsEl: HTMLElement | null = null;
|
|
117
123
|
|
|
118
124
|
// ── Smooth streaming reveal ──
|
|
119
125
|
// Tokens arrive in variable-size bursts at uneven rates, so revealing text in
|
|
@@ -204,7 +210,9 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
204
210
|
greeting: this.overrides.greeting ?? this.getAttribute('greeting') ?? undefined,
|
|
205
211
|
connectionErrorMessage: this.overrides.connectionErrorMessage,
|
|
206
212
|
startOpen: this.overrides.startOpen ?? this.hasAttribute('start-open'),
|
|
213
|
+
hideBranding: this.overrides.hideBranding ?? this.hasAttribute('hide-branding'),
|
|
207
214
|
examplePrompts: this.overrides.examplePrompts,
|
|
215
|
+
showSuggestedReplies: this.overrides.showSuggestedReplies,
|
|
208
216
|
requireName: this.overrides.requireName,
|
|
209
217
|
requireEmail: this.overrides.requireEmail,
|
|
210
218
|
requirePhone: this.overrides.requirePhone,
|
|
@@ -243,10 +251,14 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
243
251
|
onInterrupt: (interrupt) => {
|
|
244
252
|
this.interrupt = interrupt;
|
|
245
253
|
this.renderInterrupt();
|
|
254
|
+
// Suggestion chips are hidden while an interrupt overlay is up.
|
|
255
|
+
this.renderSuggestions();
|
|
246
256
|
},
|
|
247
257
|
onIdentityRestore: (state) => {
|
|
248
258
|
this.identityRestore = state;
|
|
249
259
|
this.renderInterrupt();
|
|
260
|
+
// The restore overlay reuses the interrupt slot — hide chips too.
|
|
261
|
+
this.renderSuggestions();
|
|
250
262
|
},
|
|
251
263
|
});
|
|
252
264
|
if (resolved.startOpen) this.open = true;
|
|
@@ -282,7 +294,11 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
282
294
|
<span class="title">${escapeHtml(resolved.agentName)}</span>
|
|
283
295
|
<span class="status"><span class="dot off"></span><span class="status-text"></span></span>
|
|
284
296
|
</div>
|
|
285
|
-
|
|
297
|
+
${
|
|
298
|
+
resolved.hideBranding
|
|
299
|
+
? ''
|
|
300
|
+
: `<a class="powered" href="${SMOOTH_OPERATOR_URL}" target="_blank" rel="noopener noreferrer">powered by smooth-operator</a>`
|
|
301
|
+
}
|
|
286
302
|
</div>`
|
|
287
303
|
: `<div class="header">
|
|
288
304
|
<div class="avatar">${monogram}</div>
|
|
@@ -295,6 +311,7 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
295
311
|
|
|
296
312
|
// Remember starter prompts + greeting for the empty-state chips / async renders.
|
|
297
313
|
this.examplePrompts = resolved.examplePrompts;
|
|
314
|
+
this.showSuggestedReplies = resolved.showSuggestedReplies;
|
|
298
315
|
this.greeting = resolved.greeting;
|
|
299
316
|
|
|
300
317
|
// Gate the conversation behind a pre-chat identity form when required.
|
|
@@ -329,16 +346,25 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
329
346
|
<button type="submit" class="pc-submit">Start chat</button>
|
|
330
347
|
</form>
|
|
331
348
|
</div>`;
|
|
332
|
-
|
|
349
|
+
// Footer: optional "powered by" branding (hidden by hide-branding) and an
|
|
350
|
+
// optional "Restore my chats" affordance. The " · " separator only appears
|
|
351
|
+
// when both are present, and the footer is omitted entirely when neither is.
|
|
352
|
+
const brandingHtml = resolved.hideBranding
|
|
353
|
+
? ''
|
|
354
|
+
: `<a href="${SMOOTH_OPERATOR_URL}" target="_blank" rel="noopener noreferrer">powered by <b>smooth‑operator</b></a>`;
|
|
355
|
+
const restoreBtn = this.allowChatRestore ? `<button type="button" class="restore-link">Restore my chats</button>` : '';
|
|
356
|
+
const footerInner = [brandingHtml, restoreBtn].filter(Boolean).join(' · ');
|
|
357
|
+
const footerHtml = footerInner ? `<div class="footer">${footerInner}</div>` : '';
|
|
333
358
|
const chatHtml = `
|
|
334
359
|
<div class="messages"></div>
|
|
360
|
+
<div class="reply-suggestions"></div>
|
|
335
361
|
<div class="interrupt hidden"></div>
|
|
336
362
|
<div class="composer-wrap">
|
|
337
363
|
<div class="composer">
|
|
338
364
|
<textarea rows="1" placeholder="${escapeHtml(resolved.placeholder)}"></textarea>
|
|
339
365
|
<button class="send" type="button" aria-label="Send message">${ICON.send}</button>
|
|
340
366
|
</div>
|
|
341
|
-
|
|
367
|
+
${footerHtml}
|
|
342
368
|
</div>`;
|
|
343
369
|
|
|
344
370
|
const container = document.createElement('div');
|
|
@@ -369,6 +395,7 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
369
395
|
this.inputEl = container.querySelector('textarea');
|
|
370
396
|
this.sendBtn = container.querySelector('.send');
|
|
371
397
|
this.interruptEl = container.querySelector('.interrupt');
|
|
398
|
+
this.suggestionsEl = container.querySelector('.reply-suggestions');
|
|
372
399
|
|
|
373
400
|
this.launcherEl?.addEventListener('click', () => this.openChat());
|
|
374
401
|
container.querySelector('.close')?.addEventListener('click', () => this.closeChat());
|
|
@@ -920,6 +947,39 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
920
947
|
}
|
|
921
948
|
}
|
|
922
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);
|
|
923
983
|
}
|
|
924
984
|
|
|
925
985
|
// ─────────────────────── Smooth streaming reveal ───────────────────────────
|
package/src/styles.ts
CHANGED
|
@@ -239,7 +239,8 @@ ${
|
|
|
239
239
|
}
|
|
240
240
|
.close:hover { background: color-mix(in srgb, var(--sac-text) 12%, transparent); transform: translateY(1px); }
|
|
241
241
|
.close svg { width: 16px; height: 16px; opacity: .8; }
|
|
242
|
-
.powered { margin-left: auto; font-size: 10.5px; letter-spacing: .02em; opacity: .6; }
|
|
242
|
+
.powered { margin-left: auto; font-size: 10.5px; letter-spacing: .02em; opacity: .6; color: inherit; text-decoration: none; }
|
|
243
|
+
.powered:hover { opacity: .85; text-decoration: underline; }
|
|
243
244
|
.header-sep { height: 1px; margin: 0 16px; background: linear-gradient(90deg, transparent, var(--sac-border), transparent); }
|
|
244
245
|
|
|
245
246
|
/* Full-page header: taller, logo-led, no close. */
|
|
@@ -511,6 +512,8 @@ ${
|
|
|
511
512
|
color: color-mix(in srgb, var(--sac-text) 38%, transparent);
|
|
512
513
|
}
|
|
513
514
|
.footer b { font-weight: 600; color: color-mix(in srgb, var(--sac-text) 55%, transparent); }
|
|
515
|
+
.footer a { color: inherit; text-decoration: none; }
|
|
516
|
+
.footer a:hover { text-decoration: underline; }
|
|
514
517
|
|
|
515
518
|
/* ─────────────────── Pre-chat identity form ───────────────────────── */
|
|
516
519
|
.prechat { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 18px; padding: 22px 20px; }
|
|
@@ -602,6 +605,10 @@ ${
|
|
|
602
605
|
transform: translateY(-1px);
|
|
603
606
|
}
|
|
604
607
|
|
|
608
|
+
/* ───────────── Mid-conversation suggested-reply chips ─────────────── */
|
|
609
|
+
.reply-suggestions { padding: 0 14px 4px; }
|
|
610
|
+
.reply-suggestions:empty { display: none; }
|
|
611
|
+
|
|
605
612
|
/* ─────────────── OTP / tool-confirmation interrupt ────────────────── */
|
|
606
613
|
.interrupt { padding: 0 14px; }
|
|
607
614
|
.int-card {
|