Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
43e9d36
WIP: streaming tools
Haroenv Mar 31, 2026
8355da7
fix delta key
shaejaz Apr 2, 2026
929f2c0
fix loader
shaejaz Apr 2, 2026
80c4486
update tests
shaejaz Apr 2, 2026
fa6af66
add loader state to search tool
shaejaz Apr 5, 2026
c09fd2e
add loader component prop
shaejaz Apr 7, 2026
68bc15b
add js template type
shaejaz Apr 7, 2026
ddaf0ae
Merge branch 'master' into feat/streaming-tools
shaejaz Apr 7, 2026
d431b2f
update types
shaejaz Apr 7, 2026
609b7dd
add test
shaejaz Apr 7, 2026
2aa21f6
fix lint
shaejaz Apr 7, 2026
29b58fb
Merge branch 'master' into feat/streaming-tools
shaejaz Apr 7, 2026
82f571f
update bundlesize
shaejaz Apr 7, 2026
7e1d9f4
revert example
shaejaz Apr 7, 2026
dc5a32b
add option to show loader for input streaming
shaejaz Apr 8, 2026
a6acc0a
Merge branch 'master' into feat/streaming-tools
shaejaz Apr 8, 2026
108af75
update bundlesize
shaejaz Apr 8, 2026
6cdad46
fix indents
shaejaz Apr 8, 2026
08721f1
remove not needed code
shaejaz Apr 8, 2026
2098ffa
update search tool loading indicator
shaejaz Apr 8, 2026
2d97af6
default input streaming to false
shaejaz Apr 9, 2026
7c91920
move loader logic
shaejaz Apr 10, 2026
aed2537
remove search tool loading text
shaejaz Apr 10, 2026
cdea719
optionally skip json repair
shaejaz Apr 10, 2026
8a96e55
Merge branch 'master' into feat/streaming-tools
shaejaz Apr 10, 2026
e16c8c4
update bundlesize
shaejaz Apr 10, 2026
60788da
rename flag
shaejaz Apr 10, 2026
85ea478
fix whitespace
shaejaz Apr 10, 2026
3f2d686
Merge branch 'master' into feat/streaming-tools
shaejaz Apr 13, 2026
cd4e8f7
make chat loader importable
shaejaz Apr 13, 2026
770719e
fix tests
shaejaz Apr 13, 2026
1dc6516
update js template api
shaejaz Apr 13, 2026
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
4 changes: 2 additions & 2 deletions bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.production.min.js",
"maxSize": "121.8 kB"
"maxSize": "122.75 kB"
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.development.js",
"maxSize": "253.4 kB"
"maxSize": "255 kB"
},
{
"path": "packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { createButtonComponent } from '../Button';
import { MenuIcon } from './icons';

import type { ComponentProps, Renderer, VNode } from '../../types';
import type { ChatMessageLoaderProps } from './ChatMessageLoader';
import type {
AddToolResultWithOutput,
ChatMessageBase,
Expand Down Expand Up @@ -135,6 +136,10 @@ export type ChatMessageProps = ComponentProps<'article'> & {
* Array of tools available for the assistant (for tool messages)
*/
tools: ClientSideTools;
/**
* Loader component passed to tool layout components
*/
loaderComponent: (props: ChatMessageLoaderProps) => JSX.Element;
Comment thread
shaejaz marked this conversation as resolved.
Outdated
/**
* Optional suggestions element
*/
Expand Down Expand Up @@ -171,6 +176,7 @@ export function createChatMessageComponent({ createElement }: Renderer) {
indexUiState,
setIndexUiState,
onClose,
loaderComponent: LoaderComponent,
translations: userTranslations,
suggestionsElement,
...props
Expand Down Expand Up @@ -237,6 +243,13 @@ export function createChatMessageComponent({ createElement }: Renderer) {
toolCallId: toolMessage.toolCallId,
});

if (
toolMessage.state === 'input-streaming' &&
!tool.streamInput
) {
return null;
}

if (!ToolLayoutComponent) {
return null;
}
Expand All @@ -252,8 +265,9 @@ export function createChatMessageComponent({ createElement }: Renderer) {
setIndexUiState={setIndexUiState}
addToolResult={boundAddToolResult}
applyFilters={tool.applyFilters}
sendEvent={tool.sendEvent || (() => {})}
sendEvent={tool.sendEvent || (() => { })}
Comment thread
shaejaz marked this conversation as resolved.
Outdated
onClose={onClose}
loaderComponent={LoaderComponent}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function createChatMessageLoaderComponent({
return function ChatMessageLoader(userProps: ChatMessageLoaderProps) {
const { translations: userTranslations, ...props } = userProps;
const translations: Required<ChatMessageLoaderTranslations> = {
loaderText: 'Thinking...',
loaderText: '',
...userTranslations,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import { cx } from '../../lib';
import {
findTool,
getTextContent,
hasTextContent,
isPartText,
isPartTool,
} from '../../lib/utils/chat';
import { createButtonComponent } from '../Button';

Expand Down Expand Up @@ -225,6 +227,7 @@ function createDefaultMessageComponent<
onFeedback,
feedbackState,
actionsComponent,
loaderComponent,
classNames,
messageTranslations,
translations,
Expand All @@ -243,6 +246,7 @@ function createDefaultMessageComponent<
onFeedback?: (messageId: string, vote: 0 | 1) => void;
feedbackState?: Record<string, 'sending' | 0 | 1>;
actionsComponent?: ChatMessageProps['actionsComponent'];
loaderComponent: ChatMessageProps['loaderComponent'];
translations: ChatMessagesTranslations;
classNames?: Partial<ChatMessageClassNames>;
messageTranslations?: Partial<ChatMessageTranslations>;
Expand Down Expand Up @@ -326,6 +330,7 @@ function createDefaultMessageComponent<
onClose={onClose}
actions={defaultActions}
actionsComponent={actionsComponent}
loaderComponent={loaderComponent}
data-role={message.role}
classNames={classNames}
translations={messageTranslations}
Expand Down Expand Up @@ -411,15 +416,7 @@ export function createChatMessagesComponent({

const lastMessage = messages[messages.length - 1];
const lastPart = lastMessage?.parts?.[lastMessage.parts.length - 1];
const isWaitingForResponse = status === 'submitted';
const isStreamingWithNoContent = status === 'streaming' && !lastPart;
const isStreamingNonTextContent =
status === 'streaming' && lastPart && !isPartText(lastPart);

const showLoader =
isWaitingForResponse ||
isStreamingWithNoContent ||
isStreamingNonTextContent;
const showLoader = getShowLoader(status, lastPart, tools);

const DefaultMessage = MessageComponent || DefaultMessageComponent;
const DefaultLoader = LoaderComponent || DefaultLoaderComponent;
Expand Down Expand Up @@ -463,6 +460,7 @@ export function createChatMessagesComponent({
onFeedback={onFeedback}
feedbackState={feedbackState}
actionsComponent={ActionsComponent}
loaderComponent={DefaultLoader}
onClose={onClose}
translations={translations}
classNames={messageClassNames}
Expand Down Expand Up @@ -506,3 +504,27 @@ export function createChatMessagesComponent({
);
};
}

const getShowLoader = (
status: ChatStatus,
lastPart: ChatMessageBase['parts'][number] | undefined,
tools: ClientSideTools
): boolean => {
if (status !== 'submitted' && status !== 'streaming') return false;
if (status === 'submitted') return true;

if (!lastPart) return true;
if (isPartText(lastPart)) return false;

if (isPartTool(lastPart)) {
if (lastPart.state === 'output-available') return false;
if (lastPart.state === 'input-streaming') {
const tool = findTool(lastPart.type, tools);
return !tool?.streamInput;
}
Comment on lines +515 to +520
return true;
}

return true;
};

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('ChatMessage', () => {
message={{ role: 'user', id: '1', parts: [] }}
status="ready"
tools={{}}
loaderComponent={jest.fn()}
onClose={jest.fn()}
/>
);
Expand Down Expand Up @@ -66,6 +67,7 @@ describe('ChatMessage', () => {
actions: 'actions',
}}
tools={{}}
loaderComponent={jest.fn()}
onClose={jest.fn()}
/>
);
Expand Down Expand Up @@ -104,6 +106,7 @@ describe('ChatMessage', () => {
}}
status="ready"
tools={{}}
loaderComponent={jest.fn()}
onClose={jest.fn()}
/>
<ChatMessage
Expand All @@ -116,6 +119,7 @@ describe('ChatMessage', () => {
}}
status="ready"
tools={{}}
loaderComponent={jest.fn()}
onClose={jest.fn()}
/>
<ChatMessage
Expand All @@ -128,6 +132,7 @@ describe('ChatMessage', () => {
}}
status="ready"
tools={{}}
loaderComponent={jest.fn()}
onClose={jest.fn()}
/>
</div>
Expand Down Expand Up @@ -235,6 +240,7 @@ describe('ChatMessage', () => {
applyFilters: jest.fn(),
},
}}
loaderComponent={jest.fn()}
onClose={jest.fn()}
/>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ComponentProps, SendEventForHits } from '../../types';
import type { ChatMessageLoaderProps } from './ChatMessageLoader';
import type { SearchParameters } from 'algoliasearch-helper';

export type ChatStatus = 'ready' | 'submitted' | 'streaming' | 'error';
Expand Down Expand Up @@ -485,6 +486,7 @@ export type ClientSideToolComponentProps = {
onClose: () => void;
addToolResult: AddToolResultWithOutput;
applyFilters: (params: ApplyFiltersParams) => SearchParameters;
loaderComponent: (props: ChatMessageLoaderProps) => JSX.Element;
sendEvent: SendEventForHits;
};

Expand All @@ -494,6 +496,7 @@ export type ClientSideToolComponent = (

export type ClientSideTool = {
layoutComponent?: ClientSideToolComponent;
streamInput?: boolean;
addToolResult: AddToolResult;
sendEvent?: SendEventForHits;
onToolCall?: (
Expand Down
27 changes: 27 additions & 0 deletions packages/instantsearch-ui-components/src/lib/utils/chat.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { startsWith } from './startsWith';

import type { ChatMessageBase } from '../../components';
import type {
ChatToolMessage,
ClientSideTool,
ClientSideTools,
} from '../../components/chat/types';

export const getTextContent = (message: ChatMessageBase) => {
return message.parts
Expand All @@ -15,3 +22,23 @@ export const isPartText = (
): part is Extract<ChatMessageBase['parts'][number], { type: 'text' }> => {
return part.type === 'text';
};

export const isPartTool = (
part: ChatMessageBase['parts'][number]
): part is ChatToolMessage => {
return startsWith(part.type, 'tool-');
};

export const findTool = (
partType: string,
tools: ClientSideTools
): ClientSideTool | undefined => {
const toolName = partType.replace('tool-', '');
let tool: ClientSideTool | undefined = tools[toolName];
if (!tool) {
tool = Object.entries(tools).find(([key]) =>
startsWith(toolName, `${key}_`)
)?.[1];
}
return tool;
};
Loading
Loading