From 895953681672428b467265af436d32a846dd7583 Mon Sep 17 00:00:00 2001 From: Aymeric Giraudet Date: Wed, 27 May 2026 11:40:46 +0200 Subject: [PATCH] feat(examples): prompt suggestions on PDP (PoC) Add a self-contained PromptSuggestions React component to the query-suggestions example. On the PDP it sends the product hit to an Algolia Agent Studio agent, renders the returned 3 strings as chips, and on click opens the Chat widget with the prompt prefilled via the existing `openChat` utility (`referer: 'prompt-suggestions'`). System and agent prompts for the suggestions agent are documented in `examples/react/query-suggestions/prompt-suggestions-agent.md`. --- .../prompt-suggestions-agent.md | 63 +++++++++ examples/react/query-suggestions/src/App.css | 68 ++++++++++ .../react/query-suggestions/src/Product.tsx | 24 +++- .../src/PromptSuggestions.tsx | 127 ++++++++++++++++++ 4 files changed, 278 insertions(+), 4 deletions(-) create mode 100644 examples/react/query-suggestions/prompt-suggestions-agent.md create mode 100644 examples/react/query-suggestions/src/PromptSuggestions.tsx diff --git a/examples/react/query-suggestions/prompt-suggestions-agent.md b/examples/react/query-suggestions/prompt-suggestions-agent.md new file mode 100644 index 00000000000..7cce11d88fe --- /dev/null +++ b/examples/react/query-suggestions/prompt-suggestions-agent.md @@ -0,0 +1,63 @@ +# Prompt Suggestions Agent (PoC) + +Paste both prompts below into the Algolia Agent Studio agent +`46520bcb-1f38-4ceb-9511-af5648089e4b` used by `` on the PDP. + +## Recommended agent settings + +- **Model:** cheapest/fastest available (e.g. GPT-4o-mini, Claude Haiku 4.5). Quality demand is low, latency blocks the PDP render. +- **Temperature:** ~0.5 +- **Max output tokens:** ~250 +- **Streaming:** off (the client passes `&stream=false`) +- **Tools:** none + +--- + +## System prompt + +``` +You are a programmatic helper for an ecommerce site. Your entire output must be a single JSON array of strings, parseable by JSON.parse on the first try. + +Hard rules: +- No markdown, no code fences, no surrounding prose, no explanations. +- No objects, no keys — exactly a JSON array of strings. +- Use double quotes; escape inner double quotes with \". +- Match the language of the user input. Default to English when ambiguous. + +If you are ever uncertain how to respond, return: +["Tell me more about this product","What's it good for?","Are there similar options?"] +``` + +--- + +## Agent prompt + +``` +Generate exactly 3 short follow-up prompts that a shopper viewing a product detail page would send to an AI shopping assistant. + +INPUT +The user message is a JSON object describing a single product hit from an Algolia ecommerce index. Fields vary per index but commonly include some of: `name`, `description`, `brand`, `categories`, `hierarchicalCategories`, `price`, `image`, plus arbitrary attributes (`type`, `dimensions`, `colors`, `materials`, etc.). Do not assume any field exists — inspect what's actually present. + +OUTPUT SHAPE (the only valid example) +["Is this compatible with M.2 drives?","How does it compare to the Pro model?","What sizes does it come in?"] + +CONTENT RULES +1. Write each prompt as the shopper would type it to a chat assistant. First person, conversational, ends with a question mark. +2. Be specific to THIS product. Reference its real category, brand, features, or specs when those are present in the hit. Avoid generic prompts like "Tell me more about this product". +3. The 3 prompts must cover 3 DISTINCT angles — never three variations of the same question. Pick from: + - Use-case fit ("Is this good for daily commuting?") + - Compatibility / requirements ("Does this work with USB-C?") + - Comparison ("How does this compare to the Pro version?") + - Specs deep-dive ("What's the battery life?", "What materials is it made of?") + - Sizing / fit ("What size should I get if I'm 6'1\"?") + - Care / longevity ("Is this dishwasher safe?") + - Alternatives / value ("Is there a cheaper option with similar features?") +4. Keep each prompt under ~70 characters when you can. Front-load the question. +5. Don't repeat the product's exact name — the shopper is already on its page. Use "this", "it", or a short category noun ("the laptop", "these headphones"). +6. Don't invent specs or facts that aren't in the hit. If a spec is missing, ask about it ("How much does this cost?") instead of asserting a value. +7. Don't suggest prompts the page itself already obviously answers (e.g. don't ask "What color is this?" when `colors` is in the hit and likely rendered). + +FALLBACK +If the input lacks enough product information to generate useful prompts, return exactly: +["Tell me more about this product","What's it good for?","Are there similar options?"] +``` diff --git a/examples/react/query-suggestions/src/App.css b/examples/react/query-suggestions/src/App.css index c8a24948b66..494da8d3166 100644 --- a/examples/react/query-suggestions/src/App.css +++ b/examples/react/query-suggestions/src/App.css @@ -175,6 +175,74 @@ em { margin-bottom: 1rem; } +.prompt-suggestions { + margin: 0.75rem 0; +} + +.prompt-suggestions-header { + display: flex; + align-items: center; + gap: 0.4rem; + font-size: 0.8rem; + font-weight: 600; + color: #6b6b80; + margin-bottom: 0.5rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.prompt-suggestions-sparkle { + font-size: 0.9rem; +} + +.prompt-suggestions-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.prompt-suggestions-chip { + background: #fff; + border: 1px solid #d6d6e7; + border-radius: 999px; + padding: 0.4rem 0.85rem; + font: inherit; + font-size: 0.875rem; + color: #23263b; + cursor: pointer; + transition: border-color 150ms, background 150ms, transform 100ms; +} + +.prompt-suggestions-chip:hover { + border-color: rgb(130, 50, 220); + background: rgba(130, 50, 220, 0.06); +} + +.prompt-suggestions-chip:active { + transform: scale(0.98); +} + +.prompt-suggestions-skeleton { + height: 30px; + border-radius: 999px; + background: linear-gradient( + 90deg, + #ececf3 0%, + #f5f5fa 50%, + #ececf3 100% + ); + background-size: 200% 100%; + animation: prompt-suggestions-shimmer 1.2s linear infinite; +} + +@keyframes prompt-suggestions-shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + .ais-Pagination-list { flex-wrap: wrap; } diff --git a/examples/react/query-suggestions/src/Product.tsx b/examples/react/query-suggestions/src/Product.tsx index 55aa2a63134..4f2852626f2 100644 --- a/examples/react/query-suggestions/src/Product.tsx +++ b/examples/react/query-suggestions/src/Product.tsx @@ -7,15 +7,20 @@ import { InstantSearch, RelatedProducts, Carousel, + Chat, } from 'react-instantsearch'; +import { PromptSuggestions } from './PromptSuggestions'; + import './App.css'; import 'instantsearch.css/themes/satellite.css'; -const searchClient = algoliasearch( - 'latency', - '6be0576ff61c053d5f9a3225e2a90f76' -); +const APP_ID = 'latency'; +const API_KEY = '6be0576ff61c053d5f9a3225e2a90f76'; +const CHAT_AGENT_ID = 'eedef238-5468-470d-bc37-f99fa741bd25'; +const SUGGESTIONS_AGENT_ID = '46520bcb-1f38-4ceb-9511-af5648089e4b'; + +const searchClient = algoliasearch(APP_ID, API_KEY); export function Product({ pid }: { pid: string }) { return ( @@ -53,6 +58,11 @@ export function Product({ pid }: { pid: string }) { limit={6} layoutComponent={Carousel} /> + @@ -71,6 +81,12 @@ function HitComponent({ hit }: { hit: HitType }) {

{hit.name}

+

{hit.description}

diff --git a/examples/react/query-suggestions/src/PromptSuggestions.tsx b/examples/react/query-suggestions/src/PromptSuggestions.tsx new file mode 100644 index 00000000000..b8e586ec846 --- /dev/null +++ b/examples/react/query-suggestions/src/PromptSuggestions.tsx @@ -0,0 +1,127 @@ +import { openChat } from 'instantsearch.js/es/lib/chat'; +import React, { useEffect, useState } from 'react'; +import { useInstantSearch } from 'react-instantsearch'; + +import type { ChatRenderState } from 'instantsearch.js/es/connectors/chat/connectChat'; +import type { Hit } from 'instantsearch.js'; + +type Props = { + hit: Hit; + agentId: string; + appId: string; + apiKey: string; +}; + +const FALLBACK = [ + 'Tell me more about this product', + "What's it good for?", + 'Are there similar options?', +]; + +export function PromptSuggestions({ hit, agentId, appId, apiKey }: Props) { + const [suggestions, setSuggestions] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [hasError, setHasError] = useState(false); + const { indexRenderState } = useInstantSearch(); + const chatRenderState = indexRenderState.chat as + | Partial + | undefined; + + useEffect(() => { + const controller = new AbortController(); + setIsLoading(true); + setHasError(false); + setSuggestions([]); + + const endpoint = `https://${appId}.algolia.net/agent-studio/1/agents/${agentId}/completions?compatibilityMode=ai-sdk-5&stream=false`; + const body = JSON.stringify({ + messages: [ + { + id: `ps-${Date.now()}`, + createdAt: new Date().toISOString(), + role: 'user', + parts: [{ type: 'text', text: JSON.stringify(hit) }], + }, + ], + }); + + fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-algolia-application-id': appId, + 'x-algolia-api-key': apiKey, + 'x-algolia-agent': 'prompt-suggestions-poc (1.0.0)', + }, + body, + signal: controller.signal, + }) + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return response.json(); + }) + .then((data) => { + const text = data?.parts?.[1]?.text; + const parsed = typeof text === 'string' ? JSON.parse(text) : null; + const list: string[] = (Array.isArray(parsed) ? parsed : FALLBACK) + .filter((s): s is string => typeof s === 'string' && s.trim() !== '') + .slice(0, 3); + setSuggestions(list.length > 0 ? list : FALLBACK); + setIsLoading(false); + }) + .catch((error) => { + if (error.name === 'AbortError') return; + setHasError(true); + setIsLoading(false); + }); + + return () => controller.abort(); + }, [hit.objectID, agentId, appId, apiKey]); + + if (hasError) { + return null; + } + + return ( +
+
+ + ✨ + + Ask AI about this product +
+ {isLoading ? ( +
    + {[0, 1, 2].map((i) => ( +
  • + ))} +
+ ) : ( +
    + {suggestions.map((suggestion) => ( +
  • + +
  • + ))} +
+ )} +
+ ); +}