Storefront Product Cards & Chat UX
This document describes implemented storefront chat behavior: horizontal product cards under bot replies, Get More Info / View Product actions, reply text cleanup, and server-side focus product context for accurate follow-ups.
Related: RAG & chat API · Admin settings & chat widget · Store currency · RAG retrieval — published products · Vector RAG call stack · Product sync
Overview
Section titled “Overview”When the assistant recommends products, the chat API can return a products array. The theme extension renders a horizontal card slider below the bot message (image, title, price, two buttons). The message text still includes names and prices; tags and product page links are stripped when cards are shown so shoppers use the cards for links and metadata.
Which products get cards (criteria)
Section titled “Which products get cards (criteria)”Cards are built from the assistant reply text only — not from RAG chunks, chat history, or other retrieved context. Only published, active catalog products match (draft/unpublished SKUs are excluded; zero stock is OK).
- Titles parsed from the reply (title line before
Price:, markdown product links). - Product URLs in the reply (if any).
- Full catalog title must appear in the reply (strict substring — shared prefixes like
The Collection Snowboardwithout the variant do not match sibling products). - Matched only to published, active products with a storefront URL.
- Up to 6 cards, ordered by first mention in the reply.
The model is instructed to use a fixed multi-product block format (title line → Price: on the next line → blank line → next product; no per-product descriptions) so the parser and product cards stay aligned.
Required LLM format (multi-product / recommendations)
Section titled “Required LLM format (multi-product / recommendations)”Each product block in the assistant reply:
{Exact catalog title — one line}Price: {symbol + amount}
{next product title}Price: {symbol + amount}Optional intro/outro prose before or after the list is fine. Defined in app/lib/prompt-builder.server.js. Parsed by title + price on consecutive lines in extractProductTitlesFromReply.
flowchart LR A[LLM reply + RAG chunks] --> B[resolveProductCardsForChat] B --> C[cleanReplyForProductCards] C --> D[API: reply + products] D --> E[chat-widget.js renders message + cards] E --> F[Get More Info → focusProduct + quoted title] E --> G[View Product → open PDP]Data model
Section titled “Data model”| Field | Table | Purpose |
|---|---|---|
image_url | products | Featured image URL from Shopify sync (featuredImage) for card thumbnails |
Migration: 20260522100000_add_product_image_url. Re-run product sync after deploy so existing rows get images.
Server modules
Section titled “Server modules”| Module | Role |
|---|---|
app/lib/product-cards.server.js | Resolve cards from RAG chunks + /products/ URLs in reply; toProductCardPayload (prices via formatStoreMoney + shops.currency_code); cleanReplyForProductCards (strip tags, product-type/category lines, product URLs; keep anchor text as plain name); resolveProductCardByUrl |
app/lib/rag.server.js | After LLM: resolve products, clean reply, update chat_messages.message_text if cleaned; focus product chunk boost + focusProductContext |
app/lib/prompt-builder.server.js | System rules: quoted product titles; focus product section; name + price only in text when cards show below |
app/routes/api.chat.jsx | Accepts focusProduct, pageProduct; returns { reply, sessionId, products? } |
app/routes/api.product-card.jsx | GET ?shop=&url= — lookup card by storefront product URL (widget cache / link fallback) |
Chat API — request body (storefront)
Section titled “Chat API — request body (storefront)”| Field | When | Purpose |
|---|---|---|
message | Always | User text (max 2000 chars) |
shopDomain / shop | Always | Store domain |
sessionId | Optional | Thread continuity |
appifireAiChatUserId | Recommended | Persistent visitor identity |
pageProduct | On PDP | shopifyProductId, handle, title, selectedVariantId — boosts chunks for product page |
focusProduct | Get More Info | productId, title, url, handle — one-shot focus; takes priority over pageProduct for that turn |
Chat API — response (storefront)
Section titled “Chat API — response (storefront)”{ "reply": "…assistant markdown…", "sessionId": "uuid", "products": [ { "id": "internal-product-uuid", "title": "The Multi-managed Snowboard", "url": "https://store.com/products/the-multi-managed-snowboard", "imageUrl": "https://cdn.shopify.com/…", "price": "$629.95", "priceAmount": 629.95 } ]}products is omitted when empty. Debug/RAG internals are never returned to the storefront.
Reply text cleanup (cleanReplyForProductCards)
Section titled “Reply text cleanup (cleanReplyForProductCards)”When products.length > 0:
Kept: product names, prices / price options, descriptive prose.
Removed:
- Lines matching
Tags:,Product Type:,Category:(with or without bullet prefix) - Product page URLs (markdown
[label](url)→ label text only; bare URLs removed) - Trailing orphan phrases (
you can also view here, etc.)
Not removed: full product descriptions (duplication with cards is acceptable).
Applied on server before save/response; mirrored in chat-widget.js as cleanReplyTextForProductCards for older cached responses.
Storefront widget (extensions/appifire-chat/assets/chat-widget.js)
Section titled “Storefront widget (extensions/appifire-chat/assets/chat-widget.js)”Namespace (appifire-ai-chat)
Section titled “Namespace (appifire-ai-chat)”All widget DOM ids/classes use the prefix appifire-ai-chat- (e.g. #appifire-ai-chat-window, .appifire-ai-chat-msg). localStorage keys use the same hyphen style: appifire-ai-chat-user-id, appifire-ai-chat-identity-v, appifire-ai-chat-history-{shop}. The chat API body sends appifireAiChatUserId (legacy apfcUserId still accepted server-side). Older apfc_* and appifire-ai-chat_* keys are migrated on read.
Typography (theme-independent)
Section titled “Typography (theme-independent)”- Font:
Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif - Loaded by the widget via Google Fonts (
ensureInterFont()), not from the merchant theme #appifire-ai-chat-windowand descendants use this stack with!importantso storefront themefont-familyrules do not change chat text
The embedded admin app may still use Poppins; only the storefront widget is pinned to Inter for cross-store consistency.
Product card UI
Section titled “Product card UI”- Container:
.appifire-ai-chat-product-cards(horizontal scroll, max 6 cards) - Card: image (or placeholder), title, price
- View Product (primary) —
window.open(product.url, '_blank') - Get More Info (secondary) — sends user message and
focusProduct(see below)
Product links inside the bot message HTML are display: none when cards are attached (cards remain the link affordance).
Get More Info flow
Section titled “Get More Info flow”- User-visible message:
Tell me more about "The Multi-managed Snowboard"(quotes around exact title; internal"in title →') - POST body includes:
{ "focusProduct": { "productId": "<products.id from card>", "title": "The Multi-managed Snowboard", "url": "https://…/products/…", "handle": "the-multi-managed-snowboard" }}- RAG loads product by
productIdor URL, merges product chunks first, injects Focus product (from chat product card) into the system prompt.
Other widget notes
Section titled “Other widget notes”- Cards also built from
/products/links in the bot markdown viaGET /api/product-cardwhen not inproductsarray - Powered by AppiFire branding block is commented out in markup/CSS (easy to re-enable later)
sendMessageWithText(text, sendOpts)supports optionalfocusProduct
Prompt rules (summary)
Section titled “Prompt rules (summary)”From SYSTEM_PROMPT in prompt-builder.server.js:
- Quoted product names in the customer message = exact catalog title
- Focus product section = answer only about that card-selected product
- When describing recommendations: include name and price; omit tags, product-type labels, and product URLs (cards below)
Merchant / ops checklist
Section titled “Merchant / ops checklist”- Run
npx prisma migrate deploy(includesproducts.image_url) - Re-sync products so
image_urlis populated - Deploy app + theme extension (
shopify app deployor dev) - Test: product recommendation → cards + readable reply (no broken “You can also .” sentences)
- Test: View Product opens PDP; Get More Info returns details for the same SKU
Files index
Section titled “Files index”| File |
|---|
extensions/appifire-chat/assets/chat-widget.js |
extensions/appifire-chat/blocks/chat-widget-embed.liquid (pageProduct on PDP) |
app/routes/api.chat.jsx |
app/routes/api.product-card.jsx |
app/lib/product-cards.server.js |
app/lib/rag.server.js |
app/lib/prompt-builder.server.js |
app/lib/product-sync.server.js (featuredImage → imageUrl) |
prisma/migrations/20260522100000_add_product_image_url/ |