Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 11 additions & 11 deletions bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.production.min.js",
"maxSize": "130 kB"
"maxSize": "133 kB"
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.development.js",
"maxSize": "275 kB"
"maxSize": "280 kB"
},
{
"path": "packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js",
"maxSize": "61.5 kB"
},
{
"path": "packages/react-instantsearch/dist/umd/ReactInstantSearch.min.js",
"maxSize": "102.5 kB"
"maxSize": "105.5 kB"
},
{
"path": "packages/vue-instantsearch/vue2/umd/index.js",
Expand All @@ -42,11 +42,11 @@
},
{
"path": "./packages/instantsearch.css/themes/algolia.css",
"maxSize": "10.75 kB"
"maxSize": "11.25 kB"
},
{
"path": "./packages/instantsearch.css/themes/algolia-min.css",
"maxSize": "10 kB"
"maxSize": "10.5 kB"
},
{
"path": "./packages/instantsearch.css/themes/reset.css",
Expand All @@ -58,27 +58,27 @@
},
{
"path": "./packages/instantsearch.css/themes/nova.css",
"maxSize": "11 kB"
"maxSize": "11.5 kB"
},
{
"path": "./packages/instantsearch.css/themes/nova-min.css",
"maxSize": "10.25 kB"
"maxSize": "10.75 kB"
},
{
"path": "./packages/instantsearch.css/themes/satellite.css",
"maxSize": "11.75 kB"
"maxSize": "12.25 kB"
},
{
"path": "./packages/instantsearch.css/themes/satellite-min.css",
"maxSize": "11 kB"
"maxSize": "11.5 kB"
},
{
"path": "./packages/instantsearch.css/components/chat.css",
"maxSize": "6.25 kB"
"maxSize": "6.75 kB"
},
{
"path": "./packages/instantsearch.css/components/chat-min.css",
"maxSize": "5.75 kB"
"maxSize": "6.25 kB"
},
{
"path": "./packages/instantsearch.css/components/autocomplete.css",
Expand Down
1 change: 1 addition & 0 deletions examples/react/getting-started/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export function App() {
agentId="eedef238-5468-470d-bc37-f99fa741bd25"
feedback={true}
itemComponent={ItemComponent}
showReasoning
/>
<ChatTrigger />
</InstantSearch>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { compiler } from 'markdown-to-jsx';
import { cx, startsWith } from '../../lib';
import { createButtonComponent } from '../Button';

import {
createChatMessageReasoningComponent,
type ChatMessageReasoningClassNames,
type ChatMessageReasoningTranslations,
type ChatMessageReasoningVisibility,
} from './ChatMessageReasoning';
import { MenuIcon } from './icons';

import type { ComponentProps, Renderer, VNode } from '../../types';
Expand Down Expand Up @@ -145,6 +151,30 @@ export type ChatMessageProps = ComponentProps<'article'> & {
* Optional suggestions element
*/
suggestionsElement?: VNode;
/**
* Whether to render `reasoning` UI parts via `<ChatMessageReasoning>`.
* Off by default - enable explicitly when the backend emits reasoning
* summaries / extended thinking and you want to surface them.
*/
showReasoning?: boolean;
/**
* Visibility strategy for the reasoning panel.
* - `collapsed` (default): closed, user can expand.
* - `expanded`: always open.
* - `auto`: open while streaming, collapsible afterwards.
* - `hidden`: do not render reasoning even if parts exist.
*/
reasoningVisibility?: ChatMessageReasoningVisibility;
/**
* Injectable strings for the reasoning panel (title, toggle label).
* Forwarded to `<ChatMessageReasoning>`.
*/
reasoningTranslations?: Partial<ChatMessageReasoningTranslations>;
/**
* Optional class names for the reasoning panel. Forwarded to
* `<ChatMessageReasoning>`.
*/
reasoningClassNames?: Partial<ChatMessageReasoningClassNames>;
/**
* Optional class names
*/
Expand All @@ -169,8 +199,15 @@ export type ChatMessageProps = ComponentProps<'article'> & {
// Keep in sync with packages/instantsearch.js/src/lib/chat/index.ts
const SearchIndexToolType = 'algolia_search_index';

export function createChatMessageComponent({ createElement }: Renderer) {
export function createChatMessageComponent({
createElement,
Fragment,
}: Renderer) {
const Button = createButtonComponent({ createElement });
const ChatMessageReasoning = createChatMessageReasoningComponent({
createElement,
Fragment,
});

return function ChatMessage(userProps: ChatMessageProps) {
const {
Expand All @@ -191,6 +228,10 @@ export function createChatMessageComponent({ createElement }: Renderer) {
onClose,
translations: userTranslations,
suggestionsElement,
showReasoning = false,
reasoningVisibility = 'collapsed',
reasoningTranslations,
reasoningClassNames,
parseMarkdown = true,
...props
} = userProps;
Expand Down Expand Up @@ -222,13 +263,32 @@ export function createChatMessageComponent({ createElement }: Renderer) {
footer: cx('ais-ChatMessage-footer', classNames.footer),
};

const firstReasoningIndex = showReasoning
? message.parts.findIndex((p) => p.type === 'reasoning')
: -1;

function renderMessagePart(
part: ChatMessageBase['parts'][number],
index: number
) {
if (part.type === 'step-start') {
return null;
}
if (part.type === 'reasoning') {
if (!showReasoning) return null;
// Render one reasoning panel total, anchored at the first reasoning
// part. Subsequent reasoning parts are absorbed by that panel.
if (index !== firstReasoningIndex) return null;
return (
<ChatMessageReasoning
key={`${message.id}-reasoning`}
message={message}
visibility={reasoningVisibility}
translations={reasoningTranslations}
classNames={reasoningClassNames}
/>
);
}
if (part.type === 'text') {
// Back-compat shim for sessions started before the move from a
// `<context>{...}</context>` text part to `metadata.turnContext`.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
/** @jsx createElement */

import { cx } from '../../lib';

import { LoadingSpinnerIcon } from './icons';

import type { ComponentProps, Renderer } from '../../types';

export type ChatMessageLoaderTranslations = {
/**
* Text to display in the loader
* Static text to display in the loader. Used as a fallback when no
* `reasoningPreview` is provided.
*/
loaderText?: string;
};
Expand All @@ -16,22 +19,53 @@ export type ChatMessageLoaderProps = ComponentProps<'article'> & {
* Translations for loader component texts
*/
translations?: Partial<ChatMessageLoaderTranslations>;
/**
* Live substitute label for the current reasoning step, e.g.
* "Searching the catalogue". Takes precedence over `loaderText` when set.
*/
reasoningPreview?: string;
/**
* Wall-clock time the model has been thinking, in ms. Drives the
* progress hairline; when omitted, the hairline is hidden.
*/
elapsedMs?: number;
/**
* Optimistic upper bound for the thinking time, in ms. Used to compute
* the progress ratio. Defaults to 8000 ms.
*/
expectedMs?: number;
Comment thread
afrencalg marked this conversation as resolved.
};

export function createChatMessageLoaderComponent({
createElement,
}: Pick<Renderer, 'createElement'>) {
return function ChatMessageLoader(userProps: ChatMessageLoaderProps) {
const { translations: userTranslations, ...props } = userProps;
const {
translations: userTranslations,
reasoningPreview,
elapsedMs,
expectedMs = 8000,
...props
} = userProps;
const translations: Required<ChatMessageLoaderTranslations> = {
loaderText: '',
...userTranslations,
};

const caption = reasoningPreview || translations.loaderText;
const hasProgress = typeof elapsedMs === 'number' && elapsedMs > 0;
const progress = hasProgress
? Math.max(0, Math.min(1, (elapsedMs) / expectedMs))
: 0;

return (
<article
className="ais-ChatMessageLoader ais-ChatMessage ais-ChatMessage--left ais-ChatMessage--subtle"
{...props}
className={cx(
'ais-ChatMessageLoader ais-ChatMessage ais-ChatMessage--left ais-ChatMessage--subtle',
reasoningPreview && 'ais-ChatMessageLoader--withPreview',
props.className
)}
>
<div className="ais-ChatMessage-container">
<div className="ais-ChatMessage-leading">
Expand All @@ -42,14 +76,30 @@ export function createChatMessageLoaderComponent({

<div className="ais-ChatMessage-content">
<div className="ais-ChatMessage-message">
{translations.loaderText && (
<div className="ais-ChatMessageLoader-text">
{translations.loaderText}
{caption && (
<div
className="ais-ChatMessageLoader-text"
// Re-trigger the slide+fade animation when the caption changes.
key={caption}
aria-live="polite"
>
{caption}
</div>
)}
<div className="ais-ChatMessageLoader-skeletonWrapper">
<div className="ais-ChatMessageLoader-skeletonItem"></div>
<div className="ais-ChatMessageLoader-skeletonItem"></div>
<div
className="ais-ChatMessageLoader-skeletonItem"
style={
hasProgress
? // Use the second skeleton bar as a hairline progress
// indicator when elapsedMs is provided.
({
'--ais-loader-progress': `${(progress * 100).toFixed(0)}%`,
} as Record<string, string>)
: undefined
}
></div>
</div>
</div>
</div>
Expand Down
Loading