feat(agent studio): add prompt suggestions widget#7054
Conversation
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 437 |
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Pull request overview
This PR introduces a new ChatPageSuggestions widget (prompt “pills” driven by Agent Studio tasks) across InstantSearch.js and React InstantSearch, and extends the SSR pipeline to await and hydrate chat-related async state so suggestions/messages can be embedded in server HTML and reused on hydration.
Changes:
- Add
connectChatPageSuggestions+ JS/React widget wrappers and common-widget tests (Vue explicitly unsupported). - Add SSR “server-wait promises” +
initialChatStatesserialization/hydration (Next.js + core SSR utilities). - Update shared
ChatPromptSuggestionsUI component and CSS to support loading skeletons/disabled state.
Reviewed changes
Copilot reviewed 45 out of 46 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/common/widgets/index.ts | Exports the new common widget test suite. |
| tests/common/widgets/chat-page-suggestions/templates.tsx | Common template/layout override tests for the widget. |
| tests/common/widgets/chat-page-suggestions/options.ts | Common behavioral/option tests (loading, transform, click, transport, context). |
| tests/common/widgets/chat-page-suggestions/index.ts | Registers the flavored common tests for the widget. |
| packages/vue-instantsearch/src/tests/common-widgets.test.js | Marks the widget unsupported in Vue common tests. |
| packages/react-instantsearch/src/widgets/index.ts | Exposes ChatPageSuggestions from React widgets entrypoint. |
| packages/react-instantsearch/src/widgets/ChatPageSuggestions.tsx | Implements the React widget wrapper around useChatPageSuggestions + UI component. |
| packages/react-instantsearch/src/widgets/tests/all-widgets.test.tsx | Adds widget metadata coverage to the “all widgets” snapshot list. |
| packages/react-instantsearch/src/widgets/tests/utils/all-widgets.tsx | Excludes ChatPageSuggestions from generic render assertions (renders null initially). |
| packages/react-instantsearch/src/tests/common-widgets.test.tsx | Wires the React widget into cross-flavor common widget tests. |
| packages/react-instantsearch-nextjs/src/InstantSearchNext.tsx | Adds initialChatStates support to SSR context hydration. |
| packages/react-instantsearch-nextjs/src/InitializePromise.ts | Awaits widget-registered SSR promises and injects serialized chatStates. |
| packages/react-instantsearch-nextjs/src/createInsertHTML.tsx | Injects serialized initial chat state into HTML for hydration. |
| packages/react-instantsearch-core/src/server/getServerState.tsx | Exposes initialChatStates from server state when present. |
| packages/react-instantsearch-core/src/lib/useInstantSearchApi.ts | Hydrates _initialChatStates onto the InstantSearch instance client-side. |
| packages/react-instantsearch-core/src/index.ts | Exports the new useChatPageSuggestions hook. |
| packages/react-instantsearch-core/src/connectors/useChatPageSuggestions.ts | Adds hook wrapper for connectChatPageSuggestions. |
| packages/react-instantsearch-core/src/components/InstantSearchSSRProvider.tsx | Extends SSR provider contract to include initialChatStates. |
| packages/instantsearch.js/test/createInstantSearch.ts | Updates InstantSearch test mock to include SSR wait/chat state fields. |
| packages/instantsearch.js/src/widgets/index.ts | Exposes chatPageSuggestions widget from JS widgets entrypoint. |
| packages/instantsearch.js/src/widgets/chat-page-suggestions/chat-page-suggestions.tsx | Implements the InstantSearch.js widget wrapper (Preact renderer + templates). |
| packages/instantsearch.js/src/widgets/tests/index.test.ts | Ensures the widget is creatable via the widgets index. |
| packages/instantsearch.js/src/lib/tasks/types.ts | Adds types for the Agent Studio tasks transport/credentials. |
| packages/instantsearch.js/src/lib/tasks/resolveEndpoint.ts | Resolves task endpoint + headers from either transport or credentials. |
| packages/instantsearch.js/src/lib/tasks/index.ts | Exports tasks helpers/types. |
| packages/instantsearch.js/src/lib/tasks/buildTaskPayload.ts | Builds { task, input } payload envelope. |
| packages/instantsearch.js/src/lib/tasks/buildEndpoint.ts | Constructs default Agent Studio tasks endpoint URL. |
| packages/instantsearch.js/src/lib/server.ts | SSR now waits for widget-registered promises before resolving. |
| packages/instantsearch.js/src/lib/InstantSearch.ts | Adds _initialChatStates + server-wait promise registry APIs. |
| packages/instantsearch.js/src/lib/chat/sendMessageWithContext.ts | Extracts send-with-context wrapper into reusable helper. |
| packages/instantsearch.js/src/lib/chat/openChat.ts | Extends chat referer values for new page widgets. |
| packages/instantsearch.js/src/lib/chat/createAgentTransport.ts | Introduces shared agent transport builder for chat connector. |
| packages/instantsearch.js/src/lib/chat/chat.ts | Makes chat persistence safer in SSR/non-storage environments and adds persist option. |
| packages/instantsearch.js/src/connectors/index.ts | Exports connectChatPageSuggestions. |
| packages/instantsearch.js/src/connectors/chat/connectChat.ts | Refactors transport creation + context injection to shared helpers. |
| packages/instantsearch.js/src/connectors/chat-page-suggestions/connectChatPageSuggestions.ts | New connector implementing debounced fetch + SSR snapshot/hydration + chat handoff. |
| packages/instantsearch.js/src/connectors/chat-page-suggestions/tests/connectChatPageSuggestions-test.ts | Unit tests for connector lifecycle, payloads, handoff, SSR behavior. |
| packages/instantsearch.js/src/tests/common-widgets.test.tsx | Wires JS widget into cross-flavor common widget tests. |
| packages/instantsearch.css/src/components/chat/_chat-suggestions.scss | Adds skeleton styling for prompt suggestions. |
| packages/instantsearch-ui-components/src/components/chat/ChatPromptSuggestions.tsx | Adds loading skeleton + disabled support to shared UI component. |
| examples/react/next-app-router/app/Search.tsx | Demonstrates the widget usage in Next App Router example. |
| examples/react/next-app-router/app/page.tsx | Computes absolute base URL for SSR-side fetches in example. |
| examples/react/next-app-router/app/api/chat-page-suggestions/route.ts | Adds a mock endpoint for page suggestions example. |
| examples/react/getting-started/vite.config.mjs | Adds a Vite dev-server mock endpoint for page suggestions. |
| examples/react/getting-started/src/App.tsx | Demonstrates the widget usage in the “getting-started” React example. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The example wiring isn't needed for now; reverts examples/ back to master. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
More templates
algoliasearch-helper
instantsearch-ui-components
instantsearch.css
instantsearch.js
react-instantsearch
react-instantsearch-core
react-instantsearch-nextjs
react-instantsearch-router-nextjs
vue-instantsearch
commit: |
# Conflicts: # packages/instantsearch.js/src/lib/server.ts # packages/react-instantsearch-nextjs/src/InitializePromise.ts # packages/react-instantsearch-nextjs/src/__tests__/InitializePromise.test.tsx
The SSR guard for `sessionStorage` in the Chat state is moved to its own PR (fix/chat-ssr-sessionstorage-guard) since it touches internal lib/chat files and reviews more clearly on its own. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| const DEFAULT_TRANSFORM_HITS: PromptSuggestionsTransformHits = (hits) => | ||
| hits.slice(0, 5).map(stripInternalHitMetadata); | ||
|
|
||
| function buildFilters(results: SearchResults): string[][] | undefined { |
There was a problem hiding this comment.
have we tested if this logic is worth it? wonder if it's better to send the whole search results without transforming the parameters and doing any transform later backend?
There was a problem hiding this comment.
I believe the evaluations stated that the results are sent as hits plus the query/filters/categories.
So replicated that here without sending everything.
| | 'prompt-suggestions' | ||
| | 'ai-mode' | ||
| | 'prompt-suggestions'; |
There was a problem hiding this comment.
twice the same one? one of the two should be the autocomplete prompt suggestions, the other should be the standalone one. We should be able to distinguish
There was a problem hiding this comment.
I've gone with prompt-suggestions for the widget and prompt-suggestions-autocomplete for autocomplete as it makes a bit more sense to me.
This does cause the previous prompt-suggestions associated with autocomplete to be mixed going forward. Only if we are saving this information currently. Wdyt?
Haroenv
left a comment
There was a problem hiding this comment.
couple questions about edge cases
| return suggestions.filter( | ||
| (s: unknown): s is string => typeof s === 'string' && s.trim().length > 0 | ||
| ); | ||
| } |
There was a problem hiding this comment.
why would there be an empty suggestion?
| const tasksWidget = connectTasks( | ||
| handleInnerRender, | ||
| noop | ||
| )({ | ||
| ...(transport ? { transport } : { agentId }), | ||
| task: configurationId ?? 'prompt-suggestions', | ||
| stream: true, | ||
| } as TasksConnectorParams); |
There was a problem hiding this comment.
so we create a widget, but don't explicitly render it, this seems fragile. What about returning both widgets in an array and using render state to access its items? otherwise it seems like it could cause issues later (maybe we don't call the tasks widget in all the hooks for example)
There was a problem hiding this comment.
that was how it was done initially, but i thought since the task connector is more an internal structure, maybe we don't want our renderstate to have this information.
| noop | ||
| )({ | ||
| ...(transport ? { transport } : { agentId }), | ||
| task: configurationId ?? 'prompt-suggestions', |
There was a problem hiding this comment.
I don't think we need a fallback
There was a problem hiding this comment.
unless the "first created prompt suggestion" always has that id, cc @FabienMotte, it would be possible.
There was a problem hiding this comment.
so basically have configurationId be a required prop?
| const entries = Object.entries(pageContext) | ||
| .map(([key, value]): [string, string] => [ | ||
| key, | ||
| typeof value === 'string' ? value : JSON.stringify(value), |
There was a problem hiding this comment.
should the chat context be shaped separately here? a valid task input can put hitsSample over chat’s 1 KB per-value limit, so clicking a rendered suggestion can fail with 422
There was a problem hiding this comment.
yes this is already the case. this should be rechecked once the 10kb limit is merged on the backend.
There was a problem hiding this comment.
thanks, that helps. if this PR depends on the 10 KB backend rollout, what keeps the default five-hit hitsSample within that limit?
| ? (prompt: string) => onSuggestionClickOverride(prompt, { sendToChat }) | ||
| : onSuggestionClick; | ||
|
|
||
| if (templates?.layout) { |
There was a problem hiding this comment.
should layout and header use prepareTemplateProps and TemplateComponent like other JS widgets? layout(data, { html }) currently receives only data, so the documented template throws
|
|
||
| const chatRenderState = getChatRenderState(renderOptions); | ||
|
|
||
| const isChatBusy = chatRenderState |
There was a problem hiding this comment.
should chat status transitions schedule a full InstantSearch render? after chat moves from submitted to ready, the shared render state can stay at submitted, leaving these pills disabled
There was a problem hiding this comment.
i believe this already happens, yes. will double check
| renderOptions: RenderOptions | ||
| ) => { | ||
| if (disposed || !tasksState) return; | ||
| refetchPending = false; |
There was a problem hiding this comment.
should this invalidate the active task before returning? when search state A changes to a no-hits state B, A can still resolve and render stale pills into B
There was a problem hiding this comment.
it won't render since there's now a check for this var there.
not sure about the invalidate active task, having cancellation support for that request seems a bit more complicated, perhaps in a separate PR?
There was a problem hiding this comment.
refetchPending is set to false before this return, so the guard in handleInnerRender still accepts A’s late result. could we invalidate A here without cancelling the request?
| processStream( | ||
| chunkStream, | ||
| (chunk) => { | ||
| if (!chunk || chunk.type !== 'data-task-output') { |
There was a problem hiding this comment.
should this reject when the stream emits an error event? the backend can send partial output before a terminal error but [DONE] currently resolves the last partial as success
There was a problem hiding this comment.
yeah we don't really have error states for the pills atm.
I can add that check here, but not sure how to display that to a user. For now a blank state should be okay i think
There was a problem hiding this comment.
a blank state is fine, no error UI is needed. could the error event reject the task and the prompt connector clear any streamed partial so a failed stream can’t leave partial pills visible?
FX-3845
Adds a PromptSuggestions widget that renders AI-generated prompt "pills" for the page, powered by the Agent Studio
/tasksendpoint. Clicking a pill hands the prompt off to theChatwidget on the same index.Highlights
connectPromptSuggestions(instantsearch.js,$$type: 'ais.promptSuggestions') — owns all search-aware logic: fetch on mount, 300ms debounced refetch on query/refinement change, manualrefresh(),transformItems/transformHits, and chat hand-off.connectTasksconnector — the reusable core underneath.connectPromptSuggestionsis now a thin search-aware layer on top of it, so any future task-backed feature can reuse the same request/streaming/output plumbing. Connector-only (no default UI), generic overTOutput,$$type: 'ais.tasks'; params areagentIdXORtransport,task, andstream.lib/tasks/runtime — the generic Agent Studio tasks-endpoint counterpart tolib/chat/(completions). Endpoint/credential/payload primitives (resolveEndpoint,buildTaskPayload,fetchTaskwith optional streaming,createTaskRunner, andTask*types); nothing widget-specific.context:context): auto-extracts{ query, filters, hitsSample }from the current search state and refreshes as results change.transformHitsshapes the sample (default: first 5 hits, internal_-prefixed metadata stripped).context(object or per-fetch function): sent as-is; hit auto-extraction is skipped.instantsearch-ui-components(createPromptSuggestionsComponent):.ais-PromptSuggestionspills + skeleton loading state, disabled while the chat is busy. Overridable vialayoutComponent(React) /templates.layout(JS) andonSuggestionClick.agentId(uses the InstantSearch client's app id / key) or a customtransport(api/headers).configurationIdselects the Agent Studio task (sent as thetaskfield, default'prompt_suggestions').connectPromptSuggestionsandconnectTasks,lib/tasksunit tests, and cross-flavor common tests (common-widgets -t "PromptSuggestions widget common tests"); Vue registered as skipped.React API
Default (page-driven):
Explicit context (e.g. a product page):