Skip to content
Draft
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
63 changes: 63 additions & 0 deletions examples/react/query-suggestions/prompt-suggestions-agent.md
Original file line number Diff line number Diff line change
@@ -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 `<PromptSuggestions>` 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?"]
```
68 changes: 68 additions & 0 deletions examples/react/query-suggestions/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
24 changes: 20 additions & 4 deletions examples/react/query-suggestions/src/Product.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@
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';

Check failure on line 19 in examples/react/query-suggestions/src/Product.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

examples/react/query-suggestions/src/Product.tsx#L19

Generic API Key detected
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 (
Expand Down Expand Up @@ -53,6 +58,11 @@
limit={6}
layoutComponent={Carousel}
/>
<Chat
agentId={CHAT_AGENT_ID}
feedback={true}
itemComponent={ItemComponent}
/>
</InstantSearch>
</div>
</>
Expand All @@ -71,6 +81,12 @@
<img src={hit.image} />
<div>
<h1>{hit.name}</h1>
<PromptSuggestions
hit={hit}
agentId={SUGGESTIONS_AGENT_ID}
appId={APP_ID}
apiKey={API_KEY}
/>
<p>{hit.description}</p>
</div>
</article>
Expand Down
127 changes: 127 additions & 0 deletions examples/react/query-suggestions/src/PromptSuggestions.tsx
Original file line number Diff line number Diff line change
@@ -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<string[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const { indexRenderState } = useInstantSearch();
const chatRenderState = indexRenderState.chat as
| Partial<ChatRenderState>
| 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 (
<div className="prompt-suggestions">
<div className="prompt-suggestions-header">
<span className="prompt-suggestions-sparkle" aria-hidden>
</span>
<span>Ask AI about this product</span>
</div>
{isLoading ? (
<ul className="prompt-suggestions-list">
{[0, 1, 2].map((i) => (
<li
key={i}
className="prompt-suggestions-skeleton"
style={{ width: `${110 + i * 30}px` }}
/>
))}
</ul>
) : (
<ul className="prompt-suggestions-list">
{suggestions.map((suggestion) => (
<li key={suggestion}>
<button
type="button"
className="prompt-suggestions-chip"
onClick={() =>
openChat(chatRenderState, {
message: suggestion,
referer: 'prompt-suggestions',
})
}
>
{suggestion}
</button>
</li>
))}
</ul>
)}
</div>
);
}
Loading