From 564e277db5fbc956fe9b94f2aaaaafd84ec19c98 Mon Sep 17 00:00:00 2001 From: Fabien Motte <662153+FabienMotte@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:01:16 +0100 Subject: [PATCH 1/9] poc(chat): add filter suggestions to search index tool --- examples/react/getting-started/src/App.tsx | 23 +++- .../src/components/FilterPill.tsx | 38 ++++++ .../src/components/SuggestedFilters.tsx | 69 +++++++++++ .../src/components/chat/ChatMessage.tsx | 8 ++ .../src/components/chat/ChatMessages.tsx | 9 ++ .../src/components/chat/types.ts | 4 +- .../src/components/index.ts | 2 + .../src/components/chat.scss | 1 + .../src/components/chat/_chat-message.scss | 4 + .../chat/_chat-suggested-filters.scss | 108 ++++++++++++++++++ .../src/widgets/chat/chat.tsx | 52 ++++++++- .../react-instantsearch/src/widgets/Chat.tsx | 1 + .../widgets/chat/tools/SearchIndexTool.tsx | 58 ++++++++-- 13 files changed, 363 insertions(+), 14 deletions(-) create mode 100644 packages/instantsearch-ui-components/src/components/FilterPill.tsx create mode 100644 packages/instantsearch-ui-components/src/components/SuggestedFilters.tsx create mode 100644 packages/instantsearch.css/src/components/chat/_chat-suggested-filters.scss diff --git a/examples/react/getting-started/src/App.tsx b/examples/react/getting-started/src/App.tsx index ad1b9961829..ac765dc5d42 100644 --- a/examples/react/getting-started/src/App.tsx +++ b/examples/react/getting-started/src/App.tsx @@ -20,10 +20,11 @@ import 'instantsearch.css/themes/satellite.css'; import './App.css'; -const searchClient = algoliasearch( - 'latency', - '6be0576ff61c053d5f9a3225e2a90f76' -); +const appId = 'latency'; +const apiKey = '6be0576ff61c053d5f9a3225e2a90f76'; +const agentId = '50426804-635d-48ed-90aa-0eebc8e2f878'; + +const searchClient = algoliasearch(appId, apiKey); export function App() { return ( @@ -52,6 +53,12 @@ export function App() { + + + + + +
@@ -72,8 +79,14 @@ export function App() {
diff --git a/packages/instantsearch-ui-components/src/components/FilterPill.tsx b/packages/instantsearch-ui-components/src/components/FilterPill.tsx new file mode 100644 index 00000000000..1cbacaebb1f --- /dev/null +++ b/packages/instantsearch-ui-components/src/components/FilterPill.tsx @@ -0,0 +1,38 @@ +/** @jsx createElement */ +import { cx } from '../lib/cx'; + +import type { Renderer } from '../types'; + +export type FilterPillProps = { + label: string; + value: string; + count: number; + isRefined: boolean; + onClick: () => void; + className?: string; + key?: string; +}; + +export function createFilterPillComponent({ + createElement, +}: Pick) { + return function FilterPill(props: FilterPillProps) { + const { label, value, count, isRefined, onClick, className } = props; + + return ( + + ); + }; +} diff --git a/packages/instantsearch-ui-components/src/components/SuggestedFilters.tsx b/packages/instantsearch-ui-components/src/components/SuggestedFilters.tsx new file mode 100644 index 00000000000..417d6a0b385 --- /dev/null +++ b/packages/instantsearch-ui-components/src/components/SuggestedFilters.tsx @@ -0,0 +1,69 @@ +/** @jsx createElement */ + +import { cx } from '../lib/cx'; + +import { createFilterPillComponent } from './FilterPill'; + +import type { Renderer } from '../types'; + +export type SuggestedFilter = { + label: string; + attribute: string; + value: string; + count: number; +}; + +export type SuggestedFiltersProps = { + filters: SuggestedFilter[]; + onFilterClick: (attribute: string, value: string, isRefined: boolean) => void; + indexUiState: object; + className?: string; +}; + +export function createSuggestedFiltersComponent({ + createElement, +}: Pick) { + const FilterPill = createFilterPillComponent({ + createElement, + }); + + return function SuggestedFilters(props: SuggestedFiltersProps) { + const { filters, onFilterClick, indexUiState, className } = props; + + if (filters.length === 0) { + return null; + } + + // Check if a filter is currently refined + const isRefined = (attribute: string, value: string): boolean => { + const refinementList = (indexUiState as any).refinementList; + if (!refinementList || !refinementList[attribute]) { + return false; + } + return refinementList[attribute].includes(value); + }; + + return ( +
+
Suggested Filters
+
+ {filters.map((filter) => { + const refined = isRefined(filter.attribute, filter.value); + return ( + + onFilterClick(filter.attribute, filter.value, refined) + } + /> + ); + })} +
+
+ ); + }; +} diff --git a/packages/instantsearch-ui-components/src/components/chat/ChatMessage.tsx b/packages/instantsearch-ui-components/src/components/chat/ChatMessage.tsx index 0377281db70..eb33ee963dd 100644 --- a/packages/instantsearch-ui-components/src/components/chat/ChatMessage.tsx +++ b/packages/instantsearch-ui-components/src/components/chat/ChatMessage.tsx @@ -13,6 +13,7 @@ import type { ChatToolMessage, ClientSideTools, } from './types'; +import type { DynamicToolUIPart } from 'ai'; export type ChatMessageSide = 'left' | 'right'; export type ChatMessageVariant = 'neutral' | 'subtle'; @@ -126,6 +127,10 @@ export type ChatMessageProps = ComponentProps<'article'> & { * Close the chat */ onClose: () => void; + /** + * Send a message to the chat + */ + sendMessage: (params: { text: string }) => void; /** * Array of tools available for the assistant (for tool messages) */ @@ -158,6 +163,7 @@ export function createChatMessageComponent({ createElement }: Renderer) { indexUiState, setIndexUiState, onClose, + sendMessage, translations: userTranslations, ...props } = userProps; @@ -231,6 +237,8 @@ export function createChatMessageComponent({ createElement }: Renderer) { setIndexUiState={setIndexUiState} addToolResult={boundAddToolResult} onClose={onClose} + sendMessage={sendMessage} + toolState={(part as DynamicToolUIPart).state} /> ); diff --git a/packages/instantsearch-ui-components/src/components/chat/ChatMessages.tsx b/packages/instantsearch-ui-components/src/components/chat/ChatMessages.tsx index e5125e26538..691d183142d 100644 --- a/packages/instantsearch-ui-components/src/components/chat/ChatMessages.tsx +++ b/packages/instantsearch-ui-components/src/components/chat/ChatMessages.tsx @@ -112,6 +112,10 @@ export type ChatMessagesProps< * Function to close the chat */ onClose: () => void; + /** + * Function to send a message + */ + sendMessage: (params: { text: string }) => void; /** * Optional class names */ @@ -190,6 +194,7 @@ function createDefaultMessageComponent< setIndexUiState, onReload, onClose, + sendMessage, actionsComponent, classNames, messageTranslations, @@ -204,6 +209,7 @@ function createDefaultMessageComponent< tools: ClientSideTools; onReload: (messageId?: string) => void; onClose: () => void; + sendMessage: (params: { text: string }) => void; actionsComponent?: ChatMessageProps['actionsComponent']; translations: ChatMessagesTranslations; classNames?: Partial; @@ -240,6 +246,7 @@ function createDefaultMessageComponent< indexUiState={indexUiState} setIndexUiState={setIndexUiState} onClose={onClose} + sendMessage={sendMessage} actions={defaultActions} actionsComponent={actionsComponent} data-role={message.role} @@ -284,6 +291,7 @@ export function createChatMessagesComponent({ hideScrollToBottom = false, onReload, onClose, + sendMessage, translations: userTranslations, userMessageProps, assistantMessageProps, @@ -357,6 +365,7 @@ export function createChatMessagesComponent({ onReload={onReload} actionsComponent={ActionsComponent} onClose={onClose} + sendMessage={sendMessage} translations={translations} classNames={messageClassNames} messageTranslations={messageTranslations} diff --git a/packages/instantsearch-ui-components/src/components/chat/types.ts b/packages/instantsearch-ui-components/src/components/chat/types.ts index 4c6164dc496..7fa2d21c892 100644 --- a/packages/instantsearch-ui-components/src/components/chat/types.ts +++ b/packages/instantsearch-ui-components/src/components/chat/types.ts @@ -1,4 +1,4 @@ -import type { AbstractChat, ChatInit, UIMessage } from 'ai'; +import type { AbstractChat, ChatInit, DynamicToolUIPart, UIMessage } from 'ai'; export type ChatStatus = 'ready' | 'submitted' | 'streaming' | 'error'; export type ChatRole = 'data' | 'user' | 'assistant' | 'system'; @@ -21,9 +21,11 @@ export type AddToolResultWithOutput = ( export type ClientSideToolComponentProps = { message: ChatToolMessage; indexUiState: object; + toolState: DynamicToolUIPart['state']; setIndexUiState: (state: object) => void; onClose: () => void; addToolResult: AddToolResultWithOutput; + sendMessage: (params: { text: string }) => void; }; export type ClientSideToolComponent = ( diff --git a/packages/instantsearch-ui-components/src/components/index.ts b/packages/instantsearch-ui-components/src/components/index.ts index a1c94034225..a078e4dcd97 100644 --- a/packages/instantsearch-ui-components/src/components/index.ts +++ b/packages/instantsearch-ui-components/src/components/index.ts @@ -11,9 +11,11 @@ export * from './chat/ChatPrompt'; export * from './chat/ChatToggleButton'; export * from './chat/icons'; export * from './chat/types'; +export * from './FilterPill'; export * from './FrequentlyBoughtTogether'; export * from './Highlight'; export * from './Hits'; export * from './LookingSimilar'; export * from './RelatedProducts'; +export * from './SuggestedFilters'; export * from './TrendingItems'; diff --git a/packages/instantsearch.css/src/components/chat.scss b/packages/instantsearch.css/src/components/chat.scss index 2b596587d6b..3b9771ecdbc 100644 --- a/packages/instantsearch.css/src/components/chat.scss +++ b/packages/instantsearch.css/src/components/chat.scss @@ -10,3 +10,4 @@ @use 'chat/chat-message-loader'; @use 'chat/chat-prompt'; @use 'chat/chat-carousel'; +@use 'chat/chat-suggested-filters'; diff --git a/packages/instantsearch.css/src/components/chat/_chat-message.scss b/packages/instantsearch.css/src/components/chat/_chat-message.scss index 44f38237689..0aef856dc54 100644 --- a/packages/instantsearch.css/src/components/chat/_chat-message.scss +++ b/packages/instantsearch.css/src/components/chat/_chat-message.scss @@ -41,6 +41,10 @@ min-width: 0; } +.ais-ChatMessage:has(.ais-ChatMessageLoader) .ais-ChatMessage-content { + width: 100%; +} + .ais-ChatMessage-message { position: relative; text-wrap: pretty; diff --git a/packages/instantsearch.css/src/components/chat/_chat-suggested-filters.scss b/packages/instantsearch.css/src/components/chat/_chat-suggested-filters.scss new file mode 100644 index 00000000000..2f10c3495a3 --- /dev/null +++ b/packages/instantsearch.css/src/components/chat/_chat-suggested-filters.scss @@ -0,0 +1,108 @@ +@use '../../shared/_variables'; +@use '../../shared/_common'; + +.ais-ChatMessage-message { + // Suggested filters container + .ais-ChatToolSuggestedFilters { + display: flex; + flex-direction: column; + gap: calc(var(--ais-spacing) * 0.25); + margin: calc(var(--ais-spacing) * 1.5) 0 var(--ais-spacing) 0; + } + + .ais-SuggestedFilters-header { + font-size: calc(var(--ais-font-size) * 0.75); + font-weight: var(--ais-font-weight-semibold); + color: rgba(var(--ais-text-color-rgb), var(--ais-text-color-alpha)); + margin-bottom: calc(var(--ais-spacing) * 0.25); + } + + // Horizontal scrolling container + .ais-SuggestedFilters { + @extend .ais-Scrollbar; + + display: flex; + flex-wrap: nowrap; + gap: calc(var(--ais-spacing) * 0.5); + margin-left: calc(-1 * var(--ais-spacing)); + margin-right: calc(-1 * var(--ais-spacing)); + padding-left: calc(var(--ais-spacing) * 0.5); + padding-right: calc(var(--ais-spacing) * 0.5); + padding-bottom: calc(var(--ais-spacing) * 0.5); + overflow-x: auto; + overflow-y: hidden; + } + + // Individual filter pill + .ais-FilterPill { + @extend %ais-focus-outline; + + appearance: none; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: calc(var(--ais-spacing) * 0.25); + margin: 0; + padding: calc(var(--ais-spacing) * 0.25) calc(var(--ais-spacing) * 0.75); + flex-shrink: 0; + background-color: rgba( + var(--ais-background-color-rgb), + var(--ais-background-color-alpha) + ); + border: 1px solid + color-mix( + in srgb, + rgb(var(--ais-muted-color-rgb)) 30%, + rgb(var(--ais-background-color-rgb)) + ); + border-radius: calc(var(--ais-border-radius-md) * 2); + font-family: inherit; + font-size: calc(var(--ais-font-size) * 0.875); + line-height: 1.5; + color: rgba(var(--ais-text-color-rgb), var(--ais-text-color-alpha)); + + @media (prefers-reduced-motion: no-preference) { + transition: all var(--ais-transition-duration) + var(--ais-transition-timing-function); + } + + @media (hover: hover) { + &:hover { + border-color: rgba(var(--ais-primary-color-rgb), 0.5); + background-color: rgba(var(--ais-primary-color-rgb), 0.05); + } + } + + &:active { + transform: scale(0.98); + } + } + + // Filter pill sub-elements + .ais-FilterPill-label { + font-weight: var(--ais-font-weight-semibold); + text-transform: capitalize; + } + + .ais-FilterPill-value { + font-weight: normal; + } + + .ais-FilterPill-count { + opacity: 0.6; + font-size: calc(var(--ais-font-size) * 0.75); + } + + // Refined state modifier + .ais-FilterPill--refined { + background-color: rgba(var(--ais-primary-color-rgb), 0.1); + border-color: rgba(var(--ais-primary-color-rgb), 1); + color: rgb(var(--ais-primary-color-rgb)); + + @media (hover: hover) { + &:hover { + background-color: rgba(var(--ais-primary-color-rgb), 0.15); + } + } + } +} diff --git a/packages/instantsearch.js/src/widgets/chat/chat.tsx b/packages/instantsearch.js/src/widgets/chat/chat.tsx index b7d94fde2ce..646106e28bb 100644 --- a/packages/instantsearch.js/src/widgets/chat/chat.tsx +++ b/packages/instantsearch.js/src/widgets/chat/chat.tsx @@ -6,6 +6,8 @@ import { ChevronRightIcon, createButtonComponent, createChatComponent, + createChatMessageLoaderComponent, + createSuggestedFiltersComponent, } from 'instantsearch-ui-components'; import { Fragment, h, render } from 'preact'; import { useMemo } from 'preact/hooks'; @@ -81,11 +83,21 @@ function createCarouselTool< createElement: h, }); + const SuggestedFilters = createSuggestedFiltersComponent({ + createElement: h, + }); + + const ChatMessageLoader = createChatMessageLoaderComponent({ + createElement: h, + }); + function SearchLayoutComponent({ message, indexUiState, + toolState, setIndexUiState, onClose, + sendMessage, }: ClientSideToolComponentProps) { const input = message?.input as | { @@ -98,10 +110,34 @@ function createCarouselTool< | { hits?: Array>; nbHits?: number; + suggestedFilters?: Array<{ + label: string; + attribute: string; + value: string; + count: number; + }>; } | undefined; const items = output?.hits || []; + const suggestedFilters = output?.suggestedFilters || []; + + const handleFilterClick = ( + attribute: string, + value: string, + isRefined: boolean + ) => { + const action = isRefined ? 'Remove' : 'Apply'; + sendMessage({ + text: `${action} the ${attribute} filter: ${value}`, + }); + }; + + if (toolState === 'input-streaming') { + return ( + + ); + } const MemoedHeaderComponent = useMemo(() => { return ( @@ -136,7 +172,7 @@ function createCarouselTool< onClose, ]); - return carousel({ + const carouselElement = carousel({ showNavigation: false, templates: { header: MemoedHeaderComponent, @@ -155,6 +191,19 @@ function createCarouselTool< }, sendEvent: () => {}, }); + + return ( + + {carouselElement} + {suggestedFilters.length > 0 && ( + + )} + + ); } function HeaderComponent({ @@ -399,6 +448,7 @@ function ChatWrapper({ status: chatStatus, onReload: (messageId) => regenerate({ messageId }), onClose: () => setChatOpen(false), + sendMessage, messages: chatMessages, indexUiState, isClearing, diff --git a/packages/react-instantsearch/src/widgets/Chat.tsx b/packages/react-instantsearch/src/widgets/Chat.tsx index 44a8fbe73f2..0603b4c7162 100644 --- a/packages/react-instantsearch/src/widgets/Chat.tsx +++ b/packages/react-instantsearch/src/widgets/Chat.tsx @@ -243,6 +243,7 @@ export function Chat< status, onReload: (messageId) => regenerate({ messageId }), onClose: () => setOpen(false), + sendMessage, messages, tools: toolsFromConnector, indexUiState, diff --git a/packages/react-instantsearch/src/widgets/chat/tools/SearchIndexTool.tsx b/packages/react-instantsearch/src/widgets/chat/tools/SearchIndexTool.tsx index 865b52832ad..6516a8d9ee7 100644 --- a/packages/react-instantsearch/src/widgets/chat/tools/SearchIndexTool.tsx +++ b/packages/react-instantsearch/src/widgets/chat/tools/SearchIndexTool.tsx @@ -3,6 +3,8 @@ import { ChevronRightIcon, ArrowRightIcon, createButtonComponent, + createSuggestedFiltersComponent, + createChatMessageLoaderComponent, } from 'instantsearch-ui-components'; import React, { createElement } from 'react'; @@ -29,11 +31,21 @@ function createCarouselTool( createElement: createElement as Pragma, }); + const SuggestedFilters = createSuggestedFiltersComponent({ + createElement: createElement as Pragma, + }); + + const ChatMessageLoader = createChatMessageLoaderComponent({ + createElement: createElement as Pragma, + }); + function SearchLayoutComponent({ message, indexUiState, + toolState, setIndexUiState, onClose, + sendMessage, }: ClientSideToolComponentProps) { const input = message?.input as | { @@ -46,10 +58,27 @@ function createCarouselTool( | { hits?: Array>; nbHits?: number; + suggestedFilters?: Array<{ + label: string; + attribute: string; + value: string; + count: number; + }>; } | undefined; const items = output?.hits || []; + const suggestedFilters = output?.suggestedFilters || []; + + const handleFilterClick = React.useCallback( + (attribute: string, value: string, isRefined: boolean) => { + const action = isRefined ? 'Remove' : 'Apply'; + sendMessage({ + text: `${action} the ${attribute} filter: ${value}`, + }); + }, + [sendMessage] + ); const MemoedHeaderComponent = React.useMemo(() => { return ( @@ -84,14 +113,29 @@ function createCarouselTool( indexUiState, ]); + if (toolState === 'input-streaming') { + return ( + + ); + } + return ( - {}} - showNavigation={false} - headerComponent={MemoedHeaderComponent} - /> + <> + {}} + showNavigation={false} + headerComponent={MemoedHeaderComponent} + /> + {suggestedFilters.length > 0 && ( + + )} + ); } From 4ce5167482f98286f0a5062be518f97cf5688934 Mon Sep 17 00:00:00 2001 From: Fabien Motte <662153+FabienMotte@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:30:59 +0100 Subject: [PATCH 2/9] fix: useMemo lint --- packages/instantsearch.js/src/widgets/chat/chat.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/instantsearch.js/src/widgets/chat/chat.tsx b/packages/instantsearch.js/src/widgets/chat/chat.tsx index 646106e28bb..5268fe6b5fc 100644 --- a/packages/instantsearch.js/src/widgets/chat/chat.tsx +++ b/packages/instantsearch.js/src/widgets/chat/chat.tsx @@ -133,12 +133,6 @@ function createCarouselTool< }); }; - if (toolState === 'input-streaming') { - return ( - - ); - } - const MemoedHeaderComponent = useMemo(() => { return ( props: Omit< @@ -172,6 +166,12 @@ function createCarouselTool< onClose, ]); + if (toolState === 'input-streaming') { + return ( + + ); + } + const carouselElement = carousel({ showNavigation: false, templates: { From 5dc246f3794780115ff37a95d5b5581b70329f6b Mon Sep 17 00:00:00 2001 From: Fabien Motte <662153+FabienMotte@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:51:23 +0100 Subject: [PATCH 3/9] test: fix --- .../src/components/chat/__tests__/Chat.test.tsx | 1 + .../src/components/chat/__tests__/ChatMessage.test.tsx | 6 ++++++ .../src/components/chat/__tests__/ChatMessages.test.tsx | 3 +++ 3 files changed, 10 insertions(+) diff --git a/packages/instantsearch-ui-components/src/components/chat/__tests__/Chat.test.tsx b/packages/instantsearch-ui-components/src/components/chat/__tests__/Chat.test.tsx index 179394ac92f..521ab6056b0 100644 --- a/packages/instantsearch-ui-components/src/components/chat/__tests__/Chat.test.tsx +++ b/packages/instantsearch-ui-components/src/components/chat/__tests__/Chat.test.tsx @@ -25,6 +25,7 @@ describe('Chat', () => { tools: {}, onReload: jest.fn(), onClose: jest.fn(), + sendMessage: jest.fn(), }} promptProps={{}} toggleButtonProps={{ open: true, onClick: jest.fn() }} diff --git a/packages/instantsearch-ui-components/src/components/chat/__tests__/ChatMessage.test.tsx b/packages/instantsearch-ui-components/src/components/chat/__tests__/ChatMessage.test.tsx index c6cd63e4e13..eac77080f14 100644 --- a/packages/instantsearch-ui-components/src/components/chat/__tests__/ChatMessage.test.tsx +++ b/packages/instantsearch-ui-components/src/components/chat/__tests__/ChatMessage.test.tsx @@ -21,6 +21,7 @@ describe('ChatMessage', () => { message={{ role: 'user', id: '1', parts: [] }} tools={{}} onClose={jest.fn()} + sendMessage={jest.fn()} /> ); expect(container).toMatchInlineSnapshot(` @@ -65,6 +66,7 @@ describe('ChatMessage', () => { }} tools={{}} onClose={jest.fn()} + sendMessage={jest.fn()} /> ); expect(container).toMatchInlineSnapshot(` @@ -102,6 +104,7 @@ describe('ChatMessage', () => { }} tools={{}} onClose={jest.fn()} + sendMessage={jest.fn()} /> { }} tools={{}} onClose={jest.fn()} + sendMessage={jest.fn()} /> { }} tools={{}} onClose={jest.fn()} + sendMessage={jest.fn()} /> ); @@ -229,6 +234,7 @@ describe('ChatMessage', () => { }, }} onClose={jest.fn()} + sendMessage={jest.fn()} /> ); expect(container).toMatchInlineSnapshot(` diff --git a/packages/instantsearch-ui-components/src/components/chat/__tests__/ChatMessages.test.tsx b/packages/instantsearch-ui-components/src/components/chat/__tests__/ChatMessages.test.tsx index 0fe66d54399..b8264cf9d13 100644 --- a/packages/instantsearch-ui-components/src/components/chat/__tests__/ChatMessages.test.tsx +++ b/packages/instantsearch-ui-components/src/components/chat/__tests__/ChatMessages.test.tsx @@ -22,6 +22,7 @@ describe('ChatMessages', () => { tools={{}} onReload={jest.fn()} onClose={jest.fn()} + sendMessage={jest.fn()} /> ); @@ -82,6 +83,7 @@ describe('ChatMessages', () => { tools={{}} onReload={jest.fn()} onClose={jest.fn()} + sendMessage={jest.fn()} /> ); @@ -154,6 +156,7 @@ describe('ChatMessages', () => { tools={{}} onReload={jest.fn()} onClose={jest.fn()} + sendMessage={jest.fn()} /> ); From 87653261bb43147a46abe30f7f3c54f68f8a931d Mon Sep 17 00:00:00 2001 From: Fabien Motte <662153+FabienMotte@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:09:05 +0100 Subject: [PATCH 4/9] test: fix --- .../instantsearch-ui-components/src/components/FilterPill.tsx | 4 ++-- .../src/components/SuggestedFilters.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/instantsearch-ui-components/src/components/FilterPill.tsx b/packages/instantsearch-ui-components/src/components/FilterPill.tsx index 1cbacaebb1f..25b2a2a4bed 100644 --- a/packages/instantsearch-ui-components/src/components/FilterPill.tsx +++ b/packages/instantsearch-ui-components/src/components/FilterPill.tsx @@ -16,8 +16,8 @@ export type FilterPillProps = { export function createFilterPillComponent({ createElement, }: Pick) { - return function FilterPill(props: FilterPillProps) { - const { label, value, count, isRefined, onClick, className } = props; + return function FilterPill(userProps: FilterPillProps) { + const { label, value, count, isRefined, onClick, className } = userProps; return ( ); }; diff --git a/packages/instantsearch-ui-components/src/components/SuggestedFilters.tsx b/packages/instantsearch-ui-components/src/components/SuggestedFilters.tsx index 1a8265b0ca3..97bf65b3061 100644 --- a/packages/instantsearch-ui-components/src/components/SuggestedFilters.tsx +++ b/packages/instantsearch-ui-components/src/components/SuggestedFilters.tsx @@ -13,11 +13,26 @@ export type SuggestedFilter = { count: number; }; +export type SuggestedFiltersClassNames = { + /** + * Class names to apply to the root element + */ + root?: string | string[]; + /** + * Class names to apply to the header element + */ + header?: string | string[]; + /** + * Class names to apply to the filters list element + */ + list?: string | string[]; +}; + export type SuggestedFiltersProps = { filters: SuggestedFilter[]; onFilterClick: (attribute: string, value: string, isRefined: boolean) => void; indexUiState: object; - className?: string; + classNames?: Partial; }; export function createSuggestedFiltersComponent({ @@ -28,7 +43,7 @@ export function createSuggestedFiltersComponent({ }); return function SuggestedFilters(userProps: SuggestedFiltersProps) { - const { filters, onFilterClick, indexUiState, className } = userProps; + const { filters, onFilterClick, indexUiState, classNames = {} } = userProps; if (filters.length === 0) { return null; @@ -44,9 +59,11 @@ export function createSuggestedFiltersComponent({ }; return ( -
-
Suggested Filters
-
+
+
+ Suggested Filters +
+
{filters.map((filter) => { const refined = isRefined(filter.attribute, filter.value); return ( diff --git a/packages/instantsearch-ui-components/src/components/chat/ChatHeader.tsx b/packages/instantsearch-ui-components/src/components/chat/ChatHeader.tsx index a54089772c7..63ca34b654c 100644 --- a/packages/instantsearch-ui-components/src/components/chat/ChatHeader.tsx +++ b/packages/instantsearch-ui-components/src/components/chat/ChatHeader.tsx @@ -47,6 +47,10 @@ export type ChatHeaderClassNames = { * Class names to apply to the title icon element */ titleIcon?: string | string[]; + /** + * Class names to apply to the actions container element + */ + actions?: string | string[]; /** * Class names to apply to the maximize button element */ @@ -160,7 +164,7 @@ export function createChatHeaderComponent({ createElement }: Renderer) { {translations.title} -
+
{onClear && (
diff --git a/packages/instantsearch-ui-components/src/components/FilterPill.tsx b/packages/instantsearch-ui-components/src/components/FilterPill.tsx index 22350d9230b..dbee23f999f 100644 --- a/packages/instantsearch-ui-components/src/components/FilterPill.tsx +++ b/packages/instantsearch-ui-components/src/components/FilterPill.tsx @@ -55,9 +55,11 @@ export function createFilterPillComponent({ onClick={onClick} type="button" > - - {label}: - + {Boolean(label) && ( + + {label}: + + )} {value} diff --git a/packages/react-instantsearch/src/widgets/Chat.tsx b/packages/react-instantsearch/src/widgets/Chat.tsx index 0603b4c7162..1f19c4911c2 100644 --- a/packages/react-instantsearch/src/widgets/Chat.tsx +++ b/packages/react-instantsearch/src/widgets/Chat.tsx @@ -17,6 +17,7 @@ import type { ChatProps as ChatUiProps, RecommendComponentProps, RecordWithObjectID, + SuggestedFilter, UserClientSideTool, UserClientSideTools, ChatMessageProps, @@ -30,20 +31,29 @@ const ChatUiComponent = createChatComponent({ Fragment, }); +export type ChatTransformItems = { + suggestedFilters?: (items: SuggestedFilter[]) => SuggestedFilter[]; +}; + +export type { SuggestedFilter }; + export function createDefaultTools( itemComponent?: ItemComponent, - getSearchPageURL?: (nextUiState: IndexUiState) => string + getSearchPageURL?: (nextUiState: IndexUiState) => string, + transformItems?: ChatTransformItems ): UserClientSideTools { return { [SearchIndexToolType]: createCarouselTool( true, itemComponent, - getSearchPageURL + getSearchPageURL, + transformItems ), [RecommendToolType]: createCarouselTool( false, itemComponent, - getSearchPageURL + getSearchPageURL, + transformItems ), }; } @@ -99,6 +109,7 @@ export type ChatProps = Omit< itemComponent?: ItemComponent; tools?: UserClientSideTools; getSearchPageURL?: (nextUiState: IndexUiState) => string; + transformItems?: ChatTransformItems; toggleButtonProps?: UserToggleButtonProps; headerProps?: UserHeaderProps; messagesProps?: UserMessagesProps; @@ -159,6 +170,7 @@ export function Chat< translations = {}, title, getSearchPageURL, + transformItems, ...props }: ChatProps) { const { @@ -181,10 +193,14 @@ export function Chat< }); const tools = React.useMemo(() => { - const defaults = createDefaultTools(itemComponent, getSearchPageURL); + const defaults = createDefaultTools( + itemComponent, + getSearchPageURL, + transformItems + ); return { ...defaults, ...userTools }; - }, [getSearchPageURL, itemComponent, userTools]); + }, [getSearchPageURL, itemComponent, transformItems, userTools]); const chatState = useChat({ ...props, diff --git a/packages/react-instantsearch/src/widgets/chat/tools/SearchIndexTool.tsx b/packages/react-instantsearch/src/widgets/chat/tools/SearchIndexTool.tsx index 6516a8d9ee7..8da5366ff34 100644 --- a/packages/react-instantsearch/src/widgets/chat/tools/SearchIndexTool.tsx +++ b/packages/react-instantsearch/src/widgets/chat/tools/SearchIndexTool.tsx @@ -10,6 +10,7 @@ import React, { createElement } from 'react'; import { Carousel } from '../../../components'; +import type { ChatTransformItems } from '../../Chat'; import type { ClientSideToolComponentProps, Pragma, @@ -25,7 +26,8 @@ type ItemComponent = RecommendComponentProps['itemComponent']; function createCarouselTool( showViewAll: boolean, itemComponent?: ItemComponent, - getSearchPageURL?: (nextUiState: IndexUiState) => string + getSearchPageURL?: (nextUiState: IndexUiState) => string, + transformItems?: ChatTransformItems ): UserClientSideTool { const Button = createButtonComponent({ createElement: createElement as Pragma, @@ -68,7 +70,10 @@ function createCarouselTool( | undefined; const items = output?.hits || []; - const suggestedFilters = output?.suggestedFilters || []; + const rawSuggestedFilters = output?.suggestedFilters || []; + const suggestedFilters = transformItems?.suggestedFilters + ? transformItems.suggestedFilters(rawSuggestedFilters) + : rawSuggestedFilters; const handleFilterClick = React.useCallback( (attribute: string, value: string, isRefined: boolean) => { From 8962b6df9d7746f5964c79313fc130f87c17d2be Mon Sep 17 00:00:00 2001 From: Fabien Motte <662153+FabienMotte@users.noreply.github.com> Date: Mon, 29 Dec 2025 17:38:01 +0100 Subject: [PATCH 7/9] refactor: remove isRefined prop from FilterPill and SuggestedFilters components --- .../src/components/FilterPill.tsx | 16 +------- .../src/components/SuggestedFilters.tsx | 38 ++++++------------- .../chat/_chat-suggested-filters.scss | 12 ------ .../widgets/chat/tools/SearchIndexTool.tsx | 6 +-- 4 files changed, 15 insertions(+), 57 deletions(-) diff --git a/packages/instantsearch-ui-components/src/components/FilterPill.tsx b/packages/instantsearch-ui-components/src/components/FilterPill.tsx index dbee23f999f..c9277c55e9f 100644 --- a/packages/instantsearch-ui-components/src/components/FilterPill.tsx +++ b/packages/instantsearch-ui-components/src/components/FilterPill.tsx @@ -26,7 +26,6 @@ export type FilterPillProps = { label: string; value: string; count: number; - isRefined: boolean; onClick: () => void; classNames?: Partial; key?: string; @@ -36,22 +35,11 @@ export function createFilterPillComponent({ createElement, }: Pick) { return function FilterPill(userProps: FilterPillProps) { - const { - label, - value, - count, - isRefined, - onClick, - classNames = {}, - } = userProps; + const { label, value, count, onClick, classNames = {} } = userProps; return (