feat(chat): customizable loader placement, rendering and timing - #7171
Conversation
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 85 |
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Pull request overview
This PR enhances the Chat widget across the connector, UI components, and framework wrappers (InstantSearch.js + React) by making loader behavior customizable (placement, timing, and rendering context) and by adding a “suggestions loading” state with skeleton placeholders.
Changes:
- Add loader customization options (position, show override, show delay, min duration) and richer loader context (status/phase/message/tools), plus inline loader rendering.
- Introduce
suggestionsStatusinconnectChatand propagate it to UI to drive suggestion skeleton placeholders and click-guarding while loading. - Add supporting CSS (loader fade-in, inline loader styling, suggestion skeleton pills) and expand/adjust test coverage (UI + common + connector tests).
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/common/widgets/chat/options.tsx | Adds common widget tests for loader behavior (data-part trailing) and loader customization, plus suggestions placeholders behavior. |
| packages/react-instantsearch/src/widgets/Chat.tsx | Threads new loader props and suggestionsStatus into the React <Chat /> wrapper. |
| packages/instantsearch.js/src/widgets/chat/chat.tsx | Threads new loader props and suggestionsStatus into the InstantSearch.js widget renderer/templates. |
| packages/instantsearch.js/src/connectors/chat/connectChat.ts | Adds suggestionsStatus derivation logic and exposes it via render state. |
| packages/instantsearch.js/src/connectors/chat/tests/connectChat-test.ts | Adds unit tests covering suggestionsStatus state derivation. |
| packages/instantsearch.css/src/components/chat/_chat-suggestions.scss | Styles ChatPromptSuggestions skeleton placeholders. |
| packages/instantsearch.css/src/components/chat/_chat-message-loader.scss | Adds loader enter animation, reduced-motion override, and inline loader styling. |
| packages/instantsearch-ui-components/src/lib/utils/chat.ts | Adds utilities to ignore non-rendering parts for “progress” computations (findLastProgressPart). |
| packages/instantsearch-ui-components/src/components/chat/types.ts | Introduces exported loader-related types (ChatLoaderPosition, ChatLoaderPhase, ChatLoaderContext). |
| packages/instantsearch-ui-components/src/components/chat/ChatPromptSuggestions.tsx | Adds loading/skeleton rendering, blank filtering, and click suppression while loading. |
| packages/instantsearch-ui-components/src/components/chat/ChatMessages.tsx | Implements loader visibility smoothing, inline loader placement, loader context/phase, aria-busy, and “pending suggestions” mounting. |
| packages/instantsearch-ui-components/src/components/chat/ChatMessageLoader.tsx | Extends loader props to include turn context and adds inline rendering variant. |
| packages/instantsearch-ui-components/src/components/chat/ChatMessage.tsx | Adds loaderElement slot to render an inline loader under message parts. |
| packages/instantsearch-ui-components/src/components/chat/Chat.tsx | Wires suggestions loading into ChatMessages and updates hook wiring passed to the factory component. |
| packages/instantsearch-ui-components/src/components/chat/tests/ChatPromptSuggestions.test.tsx | Adds tests for skeleton rendering, blank filtering, and click suppression while loading. |
| packages/instantsearch-ui-components/src/components/chat/tests/ChatMessages.test.tsx | Adds extensive tests for loader visibility smoothing, context passing, inline rendering, and pending suggestions. |
| packages/instantsearch-ui-components/src/components/chat/tests/Chat.test.tsx | Updates factory wiring to include the new hooks passed through to ChatMessages. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
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: |
|
Size Change: +6.04 kB (+0.49%) Total Size: 1.24 MB 📦 View Changed
ℹ️ View Unchanged
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.
Suppressed comments (2)
packages/instantsearch-ui-components/src/components/chat/ChatPromptSuggestions.tsx:66
classNames.skeletonis declared inChatPromptSuggestionsClassNamesbut never applied, so consumers can’t style the loading (skeleton) state via this prop.
return (
<div className={cx('ais-ChatPromptSuggestions', classNames.root)}>
{isLoading && visibleSuggestions.length === 0
packages/instantsearch-ui-components/src/components/chat/ChatMessages.tsx:752
aria-busyis currently tied toshowLoader(the smoothed/possibly delayed visibility). If the loader is delayed/hidden (e.g. viashouldShowLoader), the log is still updating whilearia-busystays unset. Consider derivingaria-busyfrom the actual turn activity (submitted/streaming) instead of loader visibility.
<div
{...props}
className={cx(cssClasses.root, props.className)}
role="log"
aria-live="polite"
aria-busy={showLoader ? 'true' : undefined}
>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (2)
packages/instantsearch-ui-components/src/components/chat/ChatMessages.tsx:761
loaderContext.messageis set tolastMessageeven when the last message is from the user (common instatus === 'submitted'). The type docs say this should be “the message the loader belongs to, when there is one”, so passing a user message here can confuse custom loaders (e.g., expecting an assistant message or no message when the loader is rendered as its own row).
const loaderContext: ChatLoaderContext<TMessage> = {
...context,
phase: getLoaderPhase(status, lastMessage, showReasoning),
message: lastMessage,
};
packages/instantsearch-ui-components/src/components/chat/ChatMessages.tsx:506
useLoaderVisibilitymutatesstateRef.currentduring render to deriveisVisible. This pattern is not safe under React 18 concurrent rendering (renders can be started, paused, or abandoned), which can lead to inconsistent loader visibility timing whenChatMessagesis used through the React wrapper.
This issue also appears on line 757 of the same file.
const state = stateRef.current;
const now = Date.now();
if (!isTurnActive) {
state.hasHiddenInTurn = false;
}
Why
The chat loader was effectively fixed in place and hardcoded: it could only render at the end of the messages list, its display logic lived in the UI component with no way to influence it, the suggestions row appeared out of nothing at the end of a turn, and it visibly popped in and out mid-answer.
That last one had two causes. Parts that render nothing still answered for the turn's progress — a trailing
data-*payload, a text part created bytext-startbefore its first delta — whichfindLastProgressPartnow makes transparent. And the remaining sub-perceptual toggles are absorbed by timing: a loader returning mid-turn waits outloaderShowDelay, a visible one holds forloaderMinDuration, and honest long waits are untouched. Visibility is derived during render rather than from an effect, which would land a frame after the state it announces.API
ChatMessages, forwarded by both the JS widget and React<Chat>:loaderPosition'messages-end' | 'message-inline''messages-end'shouldShowLoader(ctx: ChatLoaderContext & { defaultValue: boolean }) => booleanloaderShowDelaynumber(ms)250loaderMinDurationnumber(ms)200translations.loaderTextwidens fromstringto also accept(ctx: ChatLoaderContext) => string, andloaderComponentnow receives the turn context (status,phase,message,messages,tools) plusinline.shouldShowLoadergets the built-in decision asdefaultValueso an override narrows it instead of reimplementing it.ChatMessagegainsloaderElement, so a custommessageComponentcan place the loader itself.New exported types:
ChatLoaderPosition,ChatLoaderPhase('submitted' | 'tool' | 'reasoning' | 'thinking'),ChatLoaderContext.Alongside:
aria-busyon the messages log, a loader fade-in with aprefers-reduced-motionoverride and no exit animation, and an inline loader variant without the message chrome.Suggestions
ChatPromptSuggestionsgainsisLoading,skeletonCountandclassNames.skeleton/classNames.skeletonItem, mirroring standalonePromptSuggestions; it also filters blank suggestions and ignores clicks while loading.connectChatexposessuggestionsStatus('idle' | 'loading') to drive it, inferring "still coming" from a running turn with nodata-suggestionspart yet, gated on evidence that suggestions are expected at all (metadata.suggestionsEnabled, or an earlier turn in the conversation produced some) so an agent that never sends them shows no placeholder.