Skip to content
Open
Show file tree
Hide file tree
Changes from 14 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
2 changes: 1 addition & 1 deletion bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.development.js",
"maxSize": "251.25 kB"
"maxSize": "252.75 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 @@ -252,8 +258,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 @@ -5,6 +5,7 @@ import {
getTextContent,
hasTextContent,
isPartText,
isToolPartActivelyRendering,
} from '../../lib/utils/chat';
import { createButtonComponent } from '../Button';

Expand Down Expand Up @@ -225,6 +226,7 @@ function createDefaultMessageComponent<
onFeedback,
feedbackState,
actionsComponent,
loaderComponent,
classNames,
messageTranslations,
translations,
Expand All @@ -243,6 +245,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 +329,7 @@ function createDefaultMessageComponent<
onClose={onClose}
actions={defaultActions}
actionsComponent={actionsComponent}
loaderComponent={loaderComponent}
data-role={message.role}
classNames={classNames}
translations={messageTranslations}
Expand Down Expand Up @@ -414,7 +418,7 @@ export function createChatMessagesComponent({
const isWaitingForResponse = status === 'submitted';
const isStreamingWithNoContent = status === 'streaming' && !lastPart;
const isStreamingNonTextContent =
status === 'streaming' && lastPart && !isPartText(lastPart);
status === 'streaming' && lastPart && !(isPartText(lastPart) || isToolPartActivelyRendering(lastPart));

const showLoader =
isWaitingForResponse ||
Expand Down Expand Up @@ -463,6 +467,7 @@ export function createChatMessagesComponent({
onFeedback={onFeedback}
feedbackState={feedbackState}
actionsComponent={ActionsComponent}
loaderComponent={DefaultLoader}
onClose={onClose}
translations={translations}
classNames={messageClassNames}
Expand Down
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
136 changes: 70 additions & 66 deletions packages/instantsearch-ui-components/src/components/chat/types.ts
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 @@ -120,38 +121,39 @@ export type ToolUIPart<TTools extends UITools = UITools> = ValueOf<{
toolCallId: string;
} & (
| {
state: 'input-streaming';
input: DeepPartial<TTools[NAME]['input']> | undefined;
providerExecuted?: boolean;
output?: never;
errorText?: never;
}
state: 'input-streaming';
input: DeepPartial<TTools[NAME]['input']> | undefined;
rawInput?: string;
providerExecuted?: boolean;
output?: never;
errorText?: never;
}
| {
state: 'input-available';
input: TTools[NAME]['input'];
providerExecuted?: boolean;
output?: never;
errorText?: never;
callProviderMetadata?: ProviderMetadata;
}
state: 'input-available';
input: TTools[NAME]['input'];
providerExecuted?: boolean;
output?: never;
errorText?: never;
callProviderMetadata?: ProviderMetadata;
}
| {
state: 'output-available';
input: TTools[NAME]['input'];
output: TTools[NAME]['output'];
errorText?: never;
providerExecuted?: boolean;
callProviderMetadata?: ProviderMetadata;
preliminary?: boolean;
}
state: 'output-available';
input: TTools[NAME]['input'];
output: TTools[NAME]['output'];
errorText?: never;
providerExecuted?: boolean;
callProviderMetadata?: ProviderMetadata;
preliminary?: boolean;
}
| {
state: 'output-error';
input: TTools[NAME]['input'] | undefined;
rawInput?: unknown;
output?: never;
errorText: string;
providerExecuted?: boolean;
callProviderMetadata?: ProviderMetadata;
}
state: 'output-error';
input: TTools[NAME]['input'] | undefined;
rawInput?: unknown;
output?: never;
errorText: string;
providerExecuted?: boolean;
callProviderMetadata?: ProviderMetadata;
}
);
}>;

Expand All @@ -163,35 +165,36 @@ export type DynamicToolUIPart = {
toolName: string;
toolCallId: string;
} & (
| {
| {
state: 'input-streaming';
input: unknown | undefined;
rawInput?: string;
output?: never;
errorText?: never;
}
| {
| {
state: 'input-available';
input: unknown;
output?: never;
errorText?: never;
callProviderMetadata?: ProviderMetadata;
}
| {
| {
state: 'output-available';
input: unknown;
output: unknown;
errorText?: never;
callProviderMetadata?: ProviderMetadata;
preliminary?: boolean;
}
| {
| {
state: 'output-error';
input: unknown;
output?: never;
errorText: string;
callProviderMetadata?: ProviderMetadata;
}
);
);

/**
* All possible message part types.
Expand Down Expand Up @@ -294,23 +297,23 @@ export type ChatOnErrorCallback = (error: Error) => void;
*/
export type InferUIMessageToolCall<TUIMessage extends UIMessage> =
| ValueOf<{
[NAME in keyof InferUIMessageTools<TUIMessage>]: {
toolName: NAME & string;
toolCallId: string;
input: InferUIMessageTools<TUIMessage>[NAME] extends {
input: infer INPUT;
}
? INPUT
: never;
dynamic?: false;
};
}>
| {
toolName: string;
[NAME in keyof InferUIMessageTools<TUIMessage>]: {
toolName: NAME & string;
toolCallId: string;
input: unknown;
dynamic: true;
input: InferUIMessageTools<TUIMessage>[NAME] extends {
input: infer INPUT;
}
? INPUT
: never;
dynamic?: false;
};
}>
| {
toolName: string;
toolCallId: string;
input: unknown;
dynamic: true;
};

/**
* Optional callback function that is invoked when a tool call is received.
Expand Down Expand Up @@ -398,25 +401,25 @@ export interface AbstractChat<TUIMessage extends UIMessage> {
sendMessage: (
message?:
| (Omit<TUIMessage, 'id' | 'role'> & {
id?: TUIMessage['id'];
role?: TUIMessage['role'];
text?: never;
files?: never;
messageId?: string;
})
id?: TUIMessage['id'];
role?: TUIMessage['role'];
text?: never;
files?: never;
messageId?: string;
})
| {
text: string;
files?: FileList | FileUIPart[];
metadata?: InferUIMessageMetadata<TUIMessage>;
parts?: never;
messageId?: string;
}
text: string;
files?: FileList | FileUIPart[];
metadata?: InferUIMessageMetadata<TUIMessage>;
parts?: never;
messageId?: string;
}
| {
files: FileList | FileUIPart[];
metadata?: InferUIMessageMetadata<TUIMessage>;
parts?: never;
messageId?: string;
},
files: FileList | FileUIPart[];
metadata?: InferUIMessageMetadata<TUIMessage>;
parts?: never;
messageId?: string;
},
options?: { headers?: Record<string, string> | Headers; body?: object }
) => Promise<void>;

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

Expand Down
20 changes: 20 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,7 @@
import { startsWith } from './startsWith';

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

export const getTextContent = (message: ChatMessageBase) => {
return message.parts
Expand All @@ -15,3 +18,20 @@ 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 isToolPartActivelyRendering = (
part: ChatMessageBase['parts'][number]
): boolean => {
return (
isPartTool(part) &&
(part.state === 'input-streaming' ||
part.state === 'output-available' ||
part.state === 'output-error')
);
};
Loading
Loading