Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chips-inline-ux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@smooai/chat-widget': patch
---

Suggested-reply chips now render inline in the message flow, directly under the latest assistant reply (previously a fixed slot above the composer, visually detached), and the strip is removed the instant a chip is tapped. Chips remain optional shortcuts — the composer stays live.
20 changes: 20 additions & 0 deletions src/element.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,26 @@ describe('mid-conversation suggested-reply chips', () => {
expect(chips[1]?.textContent).toBe('See pricing');
});

it('renders chips inline in the message flow, directly after the last bubble (SMOODEV-2668)', () => {
const { sr, api } = mountAnon();
api.messages = [user('u1', 'hi'), finalized('a1', 'Hello!', ['Book a demo'])];
api.renderMessages();
const messages = sr.querySelector('.messages')!;
const strip = messages.querySelector(':scope > .reply-suggestions');
expect(strip).not.toBeNull();
// The strip is the LAST child of the message flow — right under the reply.
expect(messages.lastElementChild).toBe(strip);
});

it('tapping a chip removes the strip immediately (before any new turn renders)', () => {
const { sr, api } = mountAnon();
api.controller = { send: () => {}, disconnect: () => {} };
api.messages = [user('u1', 'hi'), finalized('a1', 'Hello!', ['Book a demo'])];
api.renderMessages();
(sr.querySelector('.reply-suggestions .chip') as HTMLButtonElement).click();
expect(sr.querySelectorAll('.reply-suggestions .chip').length).toBe(0);
});

it('only shows suggestions for the LATEST message (not an earlier assistant turn)', () => {
const { sr, api } = mountAnon();
// Assistant turn with suggestions is followed by a newer user message.
Expand Down
38 changes: 21 additions & 17 deletions src/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,6 @@ export class SmoothAgentChatElement extends HTMLElement {
private inputEl: HTMLTextAreaElement | null = null;
private sendBtn: HTMLButtonElement | null = null;
private micBtn: HTMLButtonElement | null = null;
private suggestionsEl: HTMLElement | null = null;

// ── Smooth streaming reveal ──
// Tokens arrive in variable-size bursts at uneven rates, so revealing text in
Expand Down Expand Up @@ -544,7 +543,6 @@ export class SmoothAgentChatElement extends HTMLElement {
: '';
const chatHtml = `
<div class="messages"></div>
<div class="reply-suggestions"></div>
<div class="interrupt hidden"></div>
<div class="composer-wrap">
<div class="composer">
Expand Down Expand Up @@ -586,7 +584,6 @@ export class SmoothAgentChatElement extends HTMLElement {
this.sendBtn = container.querySelector('.send');
this.micBtn = container.querySelector('.mic');
this.interruptEl = container.querySelector('.interrupt');
this.suggestionsEl = container.querySelector('.reply-suggestions');

this.launcherEl?.addEventListener('click', () => this.openChat());
container.querySelector('.close')?.addEventListener('click', () => this.closeChat());
Expand Down Expand Up @@ -1204,40 +1201,47 @@ export class SmoothAgentChatElement extends HTMLElement {
this.messagesEl.appendChild(this.renderSources(msg.citations));
}
}
this.scrollToBottom(true);
this.renderSuggestions();
this.scrollToBottom(true);
}

/**
* Render (or clear) the mid-conversation suggested-reply chips under the
* composer. Shown ONLY when: the feature is enabled, no interrupt / restore
* overlay is active (those reuse the slot above the composer), and the LATEST
* message is a finalized assistant turn that carried suggestions. A new turn
* (agent or the visitor typing) appends messages, so the latest is no longer
* that assistant message and the chips clear on the next render. Chips reuse
* the empty-state `.prompts`/`.chip` styling and send via the same path.
* Render the mid-conversation suggested-reply chips INLINE in the message
* flow, directly under the latest bubble (SMOODEV-2668 — they used to live
* in a fixed slot above the composer, visually detached from the reply).
* Shown ONLY when: the feature is enabled, no interrupt / restore overlay is
* active, and the LATEST message is a finalized assistant turn that carried
* suggestions. Chips are shortcuts, never a gate — the composer stays live.
* Picking one removes the strip immediately; any new turn rebuilds the
* message list, so stale chips can't linger. Chips reuse the empty-state
* `.prompts`/`.chip` styling and send via the same path.
*/
private renderSuggestions(): void {
const el = this.suggestionsEl;
if (!el) return;
el.replaceChildren();
const host = this.messagesEl;
if (!host) return;
// Self-clearing: drop any strip from a previous render so a standalone
// call (e.g. an interrupt overlay appearing) can only ever hide chips.
host.querySelector(':scope > .reply-suggestions')?.remove();
if (!this.showSuggestedReplies) return;
// Hidden while an OTP / tool-confirmation / restore overlay is active.
if (this.interrupt || this.identityRestore.phase !== 'idle') return;
const last = this.messages[this.messages.length - 1];
if (!last || last.role !== 'assistant' || last.streaming || !last.suggestions || last.suggestions.length === 0) return;

const chips = document.createElement('div');
chips.className = 'prompts';
chips.className = 'prompts reply-suggestions';
for (const suggestion of last.suggestions) {
const chip = document.createElement('button');
chip.type = 'button';
chip.className = 'chip';
chip.textContent = suggestion;
chip.addEventListener('click', () => this.submitPrompt(suggestion));
chip.addEventListener('click', () => {
chips.remove(); // gone the moment a choice is made
this.submitPrompt(suggestion);
});
chips.appendChild(chip);
}
el.appendChild(chips);
host.appendChild(chips);
}

// ─────────────────────── Smooth streaming reveal ───────────────────────────
Expand Down
2 changes: 0 additions & 2 deletions src/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -697,8 +697,6 @@ ${
}

/* ───────────── Mid-conversation suggested-reply chips ─────────────── */
.reply-suggestions { padding: 0 14px 4px; }
.reply-suggestions:empty { display: none; }

/* ─────────────── OTP / tool-confirmation interrupt ────────────────── */
.interrupt { padding: 0 14px; }
Expand Down
Loading