From 7c7e5351ba3f3d6026e2606b21eb9caaaa861547 Mon Sep 17 00:00:00 2001 From: Shahmir Ejaz Date: Mon, 1 Dec 2025 10:30:04 +0000 Subject: [PATCH] init --- .../chat/tools/SearchIndexTool.tsx} | 280 ++++++++++-------- .../src/components/index.ts | 1 + .../src/widgets/chat/chat.tsx | 2 +- .../src/widgets/chat/search-index-tool.tsx | 58 ++++ .../widgets/chat/tools/SearchIndexTool.tsx | 214 +------------ tests/common/widgets/chat/options.tsx | 2 +- 6 files changed, 232 insertions(+), 325 deletions(-) rename packages/{instantsearch.js/src/widgets/chat/searchIndexTool.tsx => instantsearch-ui-components/src/components/chat/tools/SearchIndexTool.tsx} (50%) create mode 100644 packages/instantsearch.js/src/widgets/chat/search-index-tool.tsx diff --git a/packages/instantsearch.js/src/widgets/chat/searchIndexTool.tsx b/packages/instantsearch-ui-components/src/components/chat/tools/SearchIndexTool.tsx similarity index 50% rename from packages/instantsearch.js/src/widgets/chat/searchIndexTool.tsx rename to packages/instantsearch-ui-components/src/components/chat/tools/SearchIndexTool.tsx index 0fed5318dc..2c1c4273bc 100644 --- a/packages/instantsearch.js/src/widgets/chat/searchIndexTool.tsx +++ b/packages/instantsearch-ui-components/src/components/chat/tools/SearchIndexTool.tsx @@ -1,24 +1,15 @@ -/** @jsx h */ - -import { - ArrowRightIcon, - ChevronLeftIcon, - ChevronRightIcon, - createButtonComponent, -} from 'instantsearch-ui-components'; -import { h } from 'preact'; -import { useMemo } from 'preact/hooks'; - -import TemplateComponent from '../../components/Template/Template'; -import { carousel } from '../../templates'; - -import type { IndexUiState, IndexWidget } from '../../types'; -import type { ChatTemplates, UserClientSideToolWithTemplate } from './chat'; +/** @jsx createElement */ +import { createButtonComponent } from '../../Button'; +import { createCarouselComponent, generateCarouselId } from '../../Carousel'; +import { ArrowRightIcon, ChevronLeftIcon, ChevronRightIcon } from '../icons'; + import type { - ClientSideToolComponentProps, ComponentProps, RecordWithObjectID, -} from 'instantsearch-ui-components'; + Renderer, +} from '../../../types'; +import type { CarouselProps } from '../../Carousel'; +import type { ClientSideToolComponentProps } from '../types'; type SearchToolInput = { query: string; @@ -26,8 +17,49 @@ type SearchToolInput = { facet_filters?: string[][]; }; +type HeaderProps = { + showViewAll: boolean; + canScrollLeft: boolean; + canScrollRight: boolean; + scrollLeft: () => void; + scrollRight: () => void; + nbHits?: number; + input?: SearchToolInput; + hitsPerPage?: number; + setIndexUiState: (v: object) => void; + indexUiState: object; + onClose: () => void; + getSearchPageURL?: (nextUiState: object) => string; +}; + +export type SearchIndexToolProps = { + useMemo: (factory: () => TType, inputs: readonly unknown[]) => TType; + useRef: (initialValue: TType) => { current: TType }; + useState: ( + initialState: TType + ) => [TType, (newState: TType) => unknown]; + getSearchPageURL?: (nextUiState: object) => string; + toolProps: ClientSideToolComponentProps; + itemComponent?: CarouselProps['itemComponent']; + headerComponent?: (props: HeaderProps) => JSX.Element; + headerProps: Omit< + // @ts-expect-error + ComponentProps>, + | 'nbHits' + | 'query' + | 'hitsPerPage' + | 'setIndexUiState' + | 'indexUiState' + | 'getSearchPageURL' + | 'onClose' + >; +}; + function generateIndexUiState(input: SearchToolInput) { - const indexUiState: IndexUiState = {}; + const indexUiState: { + query?: string; + refinementList?: { [key: string]: string[] }; + } = {}; if (input.query) { indexUiState.query = input.query; @@ -50,91 +82,11 @@ function generateIndexUiState(input: SearchToolInput) { return indexUiState; } -export function createCarouselTool< - THit extends RecordWithObjectID = RecordWithObjectID ->( - showViewAll: boolean, - templates: ChatTemplates, - getSearchPageURL?: (nextUiState: IndexUiState) => string -): UserClientSideToolWithTemplate { - const Button = createButtonComponent({ - createElement: h, - }); - - function SearchLayoutComponent({ - message, - indexUiState, - setIndexUiState, - onClose, - }: ClientSideToolComponentProps) { - const input = message?.input as SearchToolInput | undefined; +function createHeaderComponent({ createElement }: Renderer) { + const Button = createButtonComponent({ createElement }); - const output = message?.output as - | { - hits?: Array>; - nbHits?: number; - } - | undefined; - - const items = output?.hits || []; - - const MemoedHeaderComponent = useMemo(() => { - return ( - props: Omit< - // @ts-expect-error - ComponentProps, - | 'nbHits' - | 'query' - | 'hitsPerPage' - | 'setIndexUiState' - | 'indexUiState' - | 'getSearchPageURL' - | 'onClose' - > - ) => ( - // @ts-expect-error - - ); - }, [ - items.length, - input, - output?.nbHits, - setIndexUiState, - indexUiState, - onClose, - ]); - - return carousel({ - showNavigation: false, - templates: { - header: MemoedHeaderComponent, - }, - })({ - items, - templates: { - item: ({ item }) => ( - - ), - }, - sendEvent: () => {}, - }); - } - - function HeaderComponent({ + return function HeaderComponent({ + showViewAll, canScrollLeft, canScrollRight, scrollLeft, @@ -145,21 +97,8 @@ export function createCarouselTool< setIndexUiState, indexUiState, onClose, - // eslint-disable-next-line no-shadow getSearchPageURL, - }: { - canScrollLeft: boolean; - canScrollRight: boolean; - scrollLeft: () => void; - scrollRight: () => void; - nbHits?: number; - input?: SearchToolInput; - hitsPerPage?: number; - setIndexUiState: IndexWidget['setIndexUiState']; - indexUiState: IndexUiState; - onClose: () => void; - getSearchPageURL?: (nextUiState: IndexUiState) => string; - }) { + }: HeaderProps) { if ((hitsPerPage ?? 0) < 1) { return null; } @@ -203,7 +142,7 @@ export function createCarouselTool< className="ais-ChatToolSearchIndexCarouselHeaderViewAll" > View all - + )} @@ -218,7 +157,7 @@ export function createCarouselTool< disabled={!canScrollLeft} className="ais-ChatToolSearchIndexCarouselHeaderScrollButton" > - + )} ); - } + }; +} + +export function createSearchIndexTool({ + createElement, + Fragment, +}: Renderer) { + const Header = createHeaderComponent({ createElement, Fragment }); + const Carousel = createCarouselComponent({ createElement, Fragment }); + + return function SearchIndexTool({ + useMemo, + useRef, + useState, + getSearchPageURL, + itemComponent: ItemComponent, + headerComponent: HeaderComponent, + toolProps: { message, indexUiState, setIndexUiState, onClose }, + headerProps, + }: SearchIndexToolProps) { + const input = message?.input as SearchToolInput | undefined; + + const output = message?.output as + | { + hits?: Array>; + nbHits?: number; + } + | undefined; + + const items = output?.hits || []; + + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(true); + + const carouselRefs: Pick< + CarouselProps, + | 'listRef' + | 'nextButtonRef' + | 'previousButtonRef' + | 'carouselIdRef' + | 'canScrollLeft' + | 'canScrollRight' + | 'setCanScrollLeft' + | 'setCanScrollRight' + > = { + listRef: useRef(null), + nextButtonRef: useRef(null), + previousButtonRef: useRef(null), + carouselIdRef: useRef(generateCarouselId()), + canScrollLeft, + canScrollRight, + setCanScrollLeft, + setCanScrollRight, + }; + + const MemoedHeader = useMemo(() => { + if (HeaderComponent) { + return HeaderComponent; + } + + return () => ( + // @ts-expect-error +
+ ); + }, [ + HeaderComponent, + output?.nbHits, + input, + items.length, + canScrollLeft, + canScrollRight, + setIndexUiState, + indexUiState, + getSearchPageURL, + onClose, + headerProps, + ]); - return { - templates: { layout: SearchLayoutComponent }, + return ( + {}} + /> + ); }; } diff --git a/packages/instantsearch-ui-components/src/components/index.ts b/packages/instantsearch-ui-components/src/components/index.ts index a1c9403422..8d87e86527 100644 --- a/packages/instantsearch-ui-components/src/components/index.ts +++ b/packages/instantsearch-ui-components/src/components/index.ts @@ -11,6 +11,7 @@ export * from './chat/ChatPrompt'; export * from './chat/ChatToggleButton'; export * from './chat/icons'; export * from './chat/types'; +export * from './chat/tools/SearchIndexTool'; export * from './FrequentlyBoughtTogether'; export * from './Highlight'; export * from './Hits'; diff --git a/packages/instantsearch.js/src/widgets/chat/chat.tsx b/packages/instantsearch.js/src/widgets/chat/chat.tsx index 31345e21de..c0818d670e 100644 --- a/packages/instantsearch.js/src/widgets/chat/chat.tsx +++ b/packages/instantsearch.js/src/widgets/chat/chat.tsx @@ -13,7 +13,7 @@ import { createDocumentationMessageGenerator, } from '../../lib/utils'; -import { createCarouselTool } from './searchIndexTool'; +import { createCarouselTool } from './search-index-tool'; import type { ChatRenderState, diff --git a/packages/instantsearch.js/src/widgets/chat/search-index-tool.tsx b/packages/instantsearch.js/src/widgets/chat/search-index-tool.tsx new file mode 100644 index 0000000000..4d3ed57334 --- /dev/null +++ b/packages/instantsearch.js/src/widgets/chat/search-index-tool.tsx @@ -0,0 +1,58 @@ +/** @jsx h */ + +import { createSearchIndexTool } from 'instantsearch-ui-components'; +import { Fragment, h } from 'preact'; +import { useMemo, useRef, useState } from 'preact/hooks'; + +import TemplateComponent from '../../components/Template/Template'; + +import type { IndexUiState } from '../../types'; +import type { ChatTemplates, UserClientSideToolWithTemplate } from './chat'; +import type { + ClientSideToolComponentProps, + RecordWithObjectID, +} from 'instantsearch-ui-components'; + +export function createCarouselTool< + THit extends RecordWithObjectID = RecordWithObjectID +>( + showViewAll: boolean, + templates: ChatTemplates, + getSearchPageURL?: (nextUiState: IndexUiState) => string +): UserClientSideToolWithTemplate { + const SearchLayoutUIComponent = createSearchIndexTool({ + createElement: h, + Fragment, + }); + + const itemComponent = templates.item + ? ({ item }: { item: THit }) => { + return ( + + ); + } + : undefined; + + const SearchLayoutComponent = (toolProps: ClientSideToolComponentProps) => { + return ( + + ); + }; + + return { + templates: { layout: SearchLayoutComponent }, + }; +} diff --git a/packages/react-instantsearch/src/widgets/chat/tools/SearchIndexTool.tsx b/packages/react-instantsearch/src/widgets/chat/tools/SearchIndexTool.tsx index 1e6c263802..722b8a3ef8 100644 --- a/packages/react-instantsearch/src/widgets/chat/tools/SearchIndexTool.tsx +++ b/packages/react-instantsearch/src/widgets/chat/tools/SearchIndexTool.tsx @@ -1,12 +1,5 @@ -import { - ChevronLeftIcon, - ChevronRightIcon, - ArrowRightIcon, - createButtonComponent, -} from 'instantsearch-ui-components'; -import React, { createElement } from 'react'; - -import { Carousel } from '../../../components'; +import { createSearchIndexTool } from 'instantsearch-ui-components'; +import React, { createElement, Fragment } from 'react'; import type { ClientSideToolComponentProps, @@ -15,212 +8,33 @@ import type { RecordWithObjectID, UserClientSideTool, } from 'instantsearch-ui-components'; -import type { IndexUiState, IndexWidget } from 'instantsearch.js'; -import type { ComponentProps } from 'react'; +import type { IndexUiState } from 'instantsearch.js'; type ItemComponent = RecommendComponentProps['itemComponent']; -type SearchToolInput = { - query: string; - number_of_results?: number; - facet_filters?: string[][]; -}; - -function generateIndexUiState(input: SearchToolInput) { - const indexUiState: IndexUiState = {}; - - if (input.query) { - indexUiState.query = input.query; - } - - if (input.facet_filters) { - indexUiState.refinementList = {}; - - input.facet_filters.forEach((facetFilter) => { - facetFilter.forEach((filter) => { - const [facet, value] = filter.split(':'); - if (!indexUiState.refinementList![facet]) { - indexUiState.refinementList![facet] = []; - } - indexUiState.refinementList![facet].push(value); - }); - }); - } - - return indexUiState; -} - function createCarouselTool( showViewAll: boolean, itemComponent?: ItemComponent, getSearchPageURL?: (nextUiState: IndexUiState) => string ): UserClientSideTool { - const Button = createButtonComponent({ + const SearchLayoutUIComponent = createSearchIndexTool({ createElement: createElement as Pragma, + Fragment, }); - function SearchLayoutComponent({ - message, - indexUiState, - setIndexUiState, - onClose, - }: ClientSideToolComponentProps) { - const input = message?.input as SearchToolInput | undefined; - - const output = message?.output as - | { - hits?: Array>; - nbHits?: number; - } - | undefined; - - const items = output?.hits || []; - - const MemoedHeaderComponent = React.useMemo(() => { - return ( - props: Omit< - ComponentProps, - | 'nbHits' - | 'query' - | 'hitsPerPage' - | 'setIndexUiState' - | 'indexUiState' - | 'getSearchPageURL' - | 'onClose' - > - ) => ( - - ); - }, [ - items.length, - input, - output?.nbHits, - setIndexUiState, - onClose, - indexUiState, - ]); - + const SearchLayoutComponent = (toolProps: ClientSideToolComponentProps) => { return ( - {}} - showNavigation={false} - headerComponent={MemoedHeaderComponent} + toolProps={toolProps} /> ); - } - - function HeaderComponent({ - canScrollLeft, - canScrollRight, - scrollLeft, - scrollRight, - nbHits, - input, - hitsPerPage, - setIndexUiState, - indexUiState, - // eslint-disable-next-line no-shadow - getSearchPageURL, - onClose, - }: { - canScrollLeft: boolean; - canScrollRight: boolean; - scrollLeft: () => void; - scrollRight: () => void; - nbHits?: number; - input?: SearchToolInput; - hitsPerPage?: number; - setIndexUiState: IndexWidget['setIndexUiState']; - indexUiState: IndexUiState; - getSearchPageURL?: (nextUiState: IndexUiState) => string; - onClose: () => void; - }) { - if ((hitsPerPage ?? 0) < 1) { - return null; - } - - return ( -
-
- {nbHits && ( -
- {hitsPerPage ?? 0} of {nbHits.toLocaleString()} result - {nbHits > 1 ? 's' : ''} -
- )} - {showViewAll && ( - - )} -
- - {(hitsPerPage ?? 0) > 2 && ( -
- - -
- )} -
- ); - } + }; return { layoutComponent: SearchLayoutComponent, diff --git a/tests/common/widgets/chat/options.tsx b/tests/common/widgets/chat/options.tsx index c958f0153f..a2a30e44cc 100644 --- a/tests/common/widgets/chat/options.tsx +++ b/tests/common/widgets/chat/options.tsx @@ -3,7 +3,7 @@ import { createSearchClient } from '@instantsearch/mocks'; import { wait } from '@instantsearch/testutils'; import userEvent from '@testing-library/user-event'; import { Chat, SearchIndexToolType } from 'instantsearch.js/es/lib/chat'; -import { createCarouselTool as jsCreateCarouselTool } from 'instantsearch.js/src/widgets/chat/searchIndexTool'; +import { createCarouselTool as jsCreateCarouselTool } from 'instantsearch.js/src/widgets/chat/search-index-tool'; import React from 'react'; import { createCarouselTool as reactCreateCarouselTool } from 'react-instantsearch/src/widgets/chat/tools/SearchIndexTool';