diff --git a/.changeset/chips-inline-ux.md b/.changeset/chips-inline-ux.md
new file mode 100644
index 0000000..1cd80f1
--- /dev/null
+++ b/.changeset/chips-inline-ux.md
@@ -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.
diff --git a/src/element.test.ts b/src/element.test.ts
index 24c4cfa..ac31c21 100644
--- a/src/element.test.ts
+++ b/src/element.test.ts
@@ -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.
diff --git a/src/element.ts b/src/element.ts
index 87aba7a..3ce271a 100644
--- a/src/element.ts
+++ b/src/element.ts
@@ -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
@@ -544,7 +543,6 @@ export class SmoothAgentChatElement extends HTMLElement {
: '';
const chatHtml = `
-
@@ -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());
@@ -1204,23 +1201,27 @@ 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;
@@ -1228,16 +1229,19 @@ export class SmoothAgentChatElement extends HTMLElement {
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 ───────────────────────────
diff --git a/src/styles.ts b/src/styles.ts
index 2afa2a5..b539687 100644
--- a/src/styles.ts
+++ b/src/styles.ts
@@ -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; }