import { createElement } from "../utils/dom"; import { AgentWidgetSession } from "../session"; import { AgentWidgetMessage, AgentWidgetSuggestionChipsConfig } from "../types"; export interface SuggestionButtons { buttons: HTMLButtonElement[]; render: ( chips: string[] | undefined, session: AgentWidgetSession, textarea: HTMLTextAreaElement, messages?: AgentWidgetMessage[], config?: AgentWidgetSuggestionChipsConfig, opts?: SuggestionRenderOptions ) => void; } export interface SuggestionRenderOptions { /** * Chips pushed by the agent's `suggest_replies` tool rather than the * static `suggestionChips` config. Skips the before-first-user-message * gate (the caller already applied the latest-turn visibility rule) and * dispatches `persona:suggestReplies:*` DOM events. */ agentPushed?: boolean; } export const createSuggestions = (container: HTMLElement): SuggestionButtons => { const suggestionButtons: HTMLButtonElement[] = []; // render() runs on every message change; only announce agent-pushed chips // when the visible set actually changes, not on each re-render pass. let lastAgentShownKey: string | null = null; const render = ( chips: string[] | undefined, session: AgentWidgetSession, textarea: HTMLTextAreaElement, messages?: AgentWidgetMessage[], chipsConfig?: AgentWidgetSuggestionChipsConfig, opts?: SuggestionRenderOptions ) => { container.innerHTML = ""; suggestionButtons.length = 0; const agentPushed = opts?.agentPushed === true; if (!agentPushed) lastAgentShownKey = null; if (!chips || !chips.length) return; // Hide config suggestions after the first user message is sent. // Agent-pushed chips skip this gate: their visibility is the caller's // latest-turn rule (last suggest_replies call with no user message after). // Use provided messages or get from session if (!agentPushed) { const messagesToCheck = messages ?? (session ? session.getMessages() : []); const hasUserMessage = messagesToCheck.some((msg) => msg.role === "user"); if (hasUserMessage) return; } const fragment = document.createDocumentFragment(); const streaming = session ? session.isStreaming() : false; // Get font family mapping function const getFontFamilyValue = (family: "sans-serif" | "serif" | "mono"): string => { switch (family) { case "serif": return 'Georgia, "Times New Roman", Times, serif'; case "mono": return '"Courier New", Courier, "Lucida Console", Monaco, monospace'; case "sans-serif": default: return '-apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif'; } }; chips.forEach((chip) => { const btn = createElement( "button", "persona-rounded-button persona-bg-persona-surface persona-px-3 persona-py-1.5 persona-text-xs persona-font-medium persona-text-persona-primary hover:persona-opacity-80 persona-cursor-pointer persona-border persona-border-persona-border" ) as HTMLButtonElement; btn.type = "button"; btn.textContent = chip; btn.disabled = streaming; // Apply typography settings if (chipsConfig?.fontFamily) { btn.style.fontFamily = getFontFamilyValue(chipsConfig.fontFamily); } if (chipsConfig?.fontWeight) { btn.style.fontWeight = chipsConfig.fontWeight; } // Apply padding settings if (chipsConfig?.paddingX) { btn.style.paddingLeft = chipsConfig.paddingX; btn.style.paddingRight = chipsConfig.paddingX; } if (chipsConfig?.paddingY) { btn.style.paddingTop = chipsConfig.paddingY; btn.style.paddingBottom = chipsConfig.paddingY; } btn.addEventListener("click", () => { if (!session || session.isStreaming()) return; textarea.value = ""; if (agentPushed) { container.dispatchEvent( new CustomEvent("persona:suggestReplies:selected", { detail: { suggestion: chip }, bubbles: true, composed: true, }) ); } session.sendMessage(chip); }); fragment.appendChild(btn); suggestionButtons.push(btn); }); container.appendChild(fragment); if (agentPushed) { const shownKey = JSON.stringify(chips); if (shownKey !== lastAgentShownKey) { lastAgentShownKey = shownKey; container.dispatchEvent( new CustomEvent("persona:suggestReplies:shown", { detail: { suggestions: [...chips] }, bubbles: true, composed: true, }) ); } } }; return { buttons: suggestionButtons, render }; };