Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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;
/**
* 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 || (() => { })}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor formatting: the fallback sendEvent handler is written as () => { } (extra space), which is inconsistent with the surrounding style and may fail formatting/lint rules. Consider normalizing it to () => {}.

Suggested change
sendEvent={tool.sendEvent || (() => { })}
sendEvent={tool.sendEvent || (() => {})}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this resolved @shaejaz?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I thought I addressed it, will fix.
Side note: it seems like the new linting is not a 100% the same as one with eslint, I think this should have been picked up by it.

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