Skip to content

Commit 8b35eb6

Browse files
committed
refactor(modal): wire modal ask-ai flow to the Typesense hook
1 parent fb24a5b commit 8b35eb6

6 files changed

Lines changed: 69 additions & 67 deletions

File tree

packages/docsearch-react/src/DocSearchModal.tsx

Lines changed: 19 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
} from '@algolia/autocomplete-core';
77
import type { InitialAskAiMessage, OnAskAiToggle } from '@docsearch/core';
88
import { useTheme } from '@docsearch/core/useTheme';
9-
import type { ChatRequestOptions } from 'ai';
109
import type { SearchResponse } from 'algoliasearch/lite';
1110
import React, { type JSX } from 'react';
1211
import type { MultiSearchRequestSchema } from 'typesense/lib/Typesense/Types';
@@ -34,7 +33,6 @@ import type {
3433
import type { AIMessage, AskAiState } from './types/AskiAi';
3534
import { useAskAi } from './useAskAi';
3635
import { useSearchClient } from './useSearchClient';
37-
import { useSuggestedQuestions } from './useSuggestedQuestions';
3836
import { useTouchEvents } from './useTouchEvents';
3937
import { useTrapFocus } from './useTrapFocus';
4038
import { groupBy, identity, noop, removeHighlightTags, isModifierEvent, scrollTo as scrollToUtils } from './utils';
@@ -468,17 +466,8 @@ export function DocSearchModal({
468466

469467
const searchClient = useSearchClient(transformSearchClient, typesenseServerConfig);
470468

471-
const askAiConfig = typeof askAi === 'object' ? askAi : null;
472-
const askAiConfigurationId = typeof askAi === 'string' ? askAi : askAiConfig?.assistantId || null;
473-
const askAiSearchParameters = askAiConfig?.searchParameters;
474-
const askAiUseStagingEnv = askAiConfig?.useStagingEnv || false;
469+
const askAiConfig = askAi ?? null;
475470
const [askAiState, setAskAiState] = React.useState<AskAiState>('initial');
476-
const suggestedQuestions = useSuggestedQuestions({
477-
assistantId: askAiConfigurationId,
478-
searchClient,
479-
suggestedQuestionsEnabled: askAiConfig?.suggestedQuestions,
480-
});
481-
const agentStudio = askAiConfig?.agentStudio ?? false;
482471

483472
// Format the `indexes` to be used until `indexName` and `searchParameters` props are fully removed.
484473
const indexes: DocSearchIndex[] = [];
@@ -505,7 +494,7 @@ export function DocSearchModal({
505494
// storage
506495
const conversations = React.useRef(
507496
createStoredConversations<StoredAskAiState>({
508-
key: `__DOCSEARCH_ASKAI_CONVERSATIONS__${askAiConfig?.indexName || defaultIndexName}`,
497+
key: `__DOCSEARCH_ASKAI_CONVERSATIONS__${defaultIndexName}`,
509498
limit: 10,
510499
}),
511500
).current;
@@ -524,14 +513,14 @@ export function DocSearchModal({
524513

525514
const [stoppedStream, setStoppedStream] = React.useState(false);
526515

527-
const { messages, status, setMessages, sendMessage, stopAskAiStreaming, askAiError, sendFeedback } = useAskAi({
528-
assistantId: askAiConfigurationId,
529-
apiKey: askAiConfig?.apiKey ?? 'testkey',
530-
appId: askAiConfig?.appId ?? 'testappid',
531-
indexName: askAiConfig?.indexName || defaultIndexName,
532-
searchParameters: askAiSearchParameters,
533-
useStagingEnv: askAiUseStagingEnv,
534-
agentStudio,
516+
const { messages, status, setMessages, sendMessage, stopAskAiStreaming, askAiError } = useAskAi({
517+
typesenseServerConfig,
518+
storageKey: `__DOCSEARCH_ASKAI_CONVERSATIONS__${defaultIndexName}`,
519+
collection: askAiConfig?.collection || defaultIndexName,
520+
conversationModelId: askAiConfig?.conversationModelId || '',
521+
queryBy: askAiConfig?.queryBy || 'embedding',
522+
excludeFields: askAiConfig?.excludeFields || 'embedding',
523+
searchParameters: askAiConfig?.searchParameters,
535524
});
536525

537526
const prevStatus = React.useRef(status);
@@ -544,13 +533,14 @@ export function DocSearchModal({
544533
// if we stopped the stream, store it on the most recent message
545534
if (stoppedStream && messages.at(-1)) {
546535
messages.at(-1)!.metadata = {
536+
...messages.at(-1)!.metadata,
547537
stopped: true,
548538
};
549539
}
550540

551541
for (const part of messages[0].parts) {
552542
if (part.type === 'text') {
553-
conversations.add(buildDummyAskAiHit(part.text, messages));
543+
conversations.add(buildDummyAskAiHit(part.text, messages, messages.at(-1)?.metadata?.conversationId));
554544
}
555545
}
556546
}
@@ -659,26 +649,9 @@ export function DocSearchModal({
659649

660650
setStoppedStream(false);
661651

662-
const messageOptions: ChatRequestOptions = {};
663-
664-
if (suggestedQuestion) {
665-
messageOptions.body = {
666-
suggestedQuestionId: suggestedQuestion.objectID,
667-
};
668-
}
669-
670-
sendMessage(
671-
{
672-
role: 'user',
673-
parts: [
674-
{
675-
type: 'text',
676-
text: query,
677-
},
678-
],
679-
},
680-
messageOptions,
681-
);
652+
void sendMessage(query, {
653+
suggestedQuestionId: suggestedQuestion?.objectID,
654+
});
682655

683656
if (dropdownRef.current) {
684657
// some test environments (like jsdom) don't implement element.scrollTo
@@ -700,13 +673,9 @@ export function DocSearchModal({
700673
);
701674

702675
// feedback handler
703-
const handleFeedbackSubmit = React.useCallback(
704-
async (messageId: string, thumbs: 0 | 1): Promise<void> => {
705-
if (!askAiConfigurationId) return;
706-
await sendFeedback(messageId, thumbs);
707-
},
708-
[askAiConfigurationId, sendFeedback],
709-
);
676+
const handleFeedbackSubmit = React.useCallback(async (_messageId: string, _thumbs: 0 | 1): Promise<void> => {
677+
return;
678+
}, []);
710679

711680
if (!autocompleteRef.current) {
712681
autocompleteRef.current = createAutocomplete({
@@ -1041,9 +1010,8 @@ export function DocSearchModal({
10411010
hasCollections={hasCollections}
10421011
askAiState={askAiState}
10431012
selectAskAiQuestion={handleSelectAskAiQuestion}
1044-
suggestedQuestions={suggestedQuestions}
1013+
suggestedQuestions={[]}
10451014
selectSuggestedQuestion={selectSuggestedQuestion}
1046-
agentStudio={agentStudio}
10471015
onAskAiToggle={onAskAiToggle}
10481016
onNewConversation={handleNewConversation}
10491017
onItemClick={(item, event) => {

packages/docsearch-react/src/ScreenState.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { UseChatHelpers } from '@ai-sdk/react';
21
import type { AutocompleteApi, AutocompleteState, BaseItem } from '@algolia/autocomplete-core';
32
import React from 'react';
43

@@ -18,7 +17,7 @@ import type { StartScreenTranslations } from './StartScreen';
1817
import { StartScreen } from './StartScreen';
1918
import type { StoredSearchPlugin } from './stored-searches';
2019
import type { InternalDocSearchHit, StoredAskAiState, StoredDocSearchHit, SuggestedQuestionHit } from './types';
21-
import type { AIMessage, AskAiState } from './types/AskiAi';
20+
import type { AIMessage, AskAiState, AskAiStatus } from './types/AskiAi';
2221

2322
export type ScreenStateTranslations = Partial<{
2423
errorScreen: ErrorScreenTranslations;
@@ -42,8 +41,8 @@ export interface ScreenStateProps<TItem extends BaseItem>
4241
inputRef: React.MutableRefObject<HTMLInputElement | null>;
4342
hitComponent: DocSearchProps['hitComponent'];
4443
indexName: DocSearchProps['typesenseCollectionName'];
45-
messages: UseChatHelpers<AIMessage>['messages'];
46-
status: UseChatHelpers<AIMessage>['status'];
44+
messages: AIMessage[];
45+
status: AskAiStatus;
4746
askAiError?: Error;
4847
disableUserPersonalization: boolean;
4948
resultsFooterComponent: DocSearchProps['resultsFooterComponent'];
@@ -56,7 +55,6 @@ export interface ScreenStateProps<TItem extends BaseItem>
5655
suggestedQuestions: SuggestedQuestionHit[];
5756
selectSuggestedQuestion: (question: SuggestedQuestionHit) => void;
5857
onNewConversation: () => void;
59-
agentStudio?: boolean;
6058
}
6159

6260
export const ScreenState = React.memo(
@@ -83,7 +81,6 @@ export const ScreenState = React.memo(
8381
status={props.status}
8482
askAiError={props.askAiError}
8583
translations={translations?.askAiScreen}
86-
agentStudio={props.agentStudio}
8784
/>
8885
);
8986
}

packages/docsearch-react/src/SearchBox.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { UseChatHelpers } from '@ai-sdk/react';
21
import type { AutocompleteApi, AutocompleteState } from '@algolia/autocomplete-core';
32
import React, { type JSX, type RefObject } from 'react';
43

@@ -16,7 +15,7 @@ import { BackIcon } from './icons/BackIcon';
1615
import { Menu } from './Menu';
1716
import { ModalHeading } from './ModalHeading';
1817
import type { InternalDocSearchHit } from './types';
19-
import type { AIMessage, AskAiState } from './types/AskiAi';
18+
import type { AskAiState, AskAiStatus } from './types/AskiAi';
2019

2120
export type SearchBoxTranslations = Partial<{
2221
clearButtonTitle: string;
@@ -49,7 +48,7 @@ interface SearchBoxProps
4948
onStopAskAiStreaming: () => Promise<void>;
5049
placeholder: string;
5150
isAskAiActive: boolean;
52-
askAiStatus: UseChatHelpers<AIMessage>['status'];
51+
askAiStatus: AskAiStatus;
5352
askAiError?: Error;
5453
isFromSelection: boolean;
5554
translations?: SearchBoxTranslations;

packages/docsearch-react/src/__tests__/api.test.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,20 @@ import '@testing-library/jest-dom/vitest';
77
import { DocSearch as DocSearchComponent } from '../DocSearch';
88
import type { DocSearchProps } from '../DocSearch';
99

10+
const typesenseServerConfig = {
11+
apiKey: 'test-key',
12+
nodes: [{ host: 'localhost', port: 8108, protocol: 'http' as const }],
13+
};
14+
1015
function DocSearch(props: Partial<DocSearchProps>): JSX.Element {
11-
return <DocSearchComponent appId="woo" apiKey="foo" indexName="bar" {...props} />;
16+
return (
17+
<DocSearchComponent
18+
typesenseCollectionName="docs"
19+
typesenseServerConfig={typesenseServerConfig}
20+
typesenseSearchParameters={{}}
21+
{...props}
22+
/>
23+
);
1224
}
1325

1426
// mock empty response
@@ -264,7 +276,7 @@ describe('api', () => {
264276

265277
describe('ask AI integration', () => {
266278
it('updates placeholder when ask AI is available', async () => {
267-
render(<DocSearch askAi="assistant" />);
279+
render(<DocSearch askAi={{ conversationModelId: 'conv-model-1' }} />);
268280

269281
await act(async () => {
270282
fireEvent.click(await screen.findByText('Search'));
@@ -276,7 +288,7 @@ describe('api', () => {
276288
it('opens ask AI screen and returns to search', async () => {
277289
render(
278290
<DocSearch
279-
askAi="assistant"
291+
askAi={{ conversationModelId: 'conv-model-1' }}
280292
transformSearchClient={(searchClient) => ({
281293
...searchClient,
282294
search: noResultSearch,
@@ -300,8 +312,9 @@ describe('api', () => {
300312

301313
expect(document.querySelector('.DocSearch-AskAiScreen')).toBeInTheDocument();
302314

303-
// could be "Answering..." or "Ask another question..."
304-
expect(screen.getByText('Answering...')).toBeInTheDocument();
315+
expect(
316+
screen.queryByText('Answering...') ?? screen.getByPlaceholderText('Ask another question...'),
317+
).toBeInTheDocument();
305318
});
306319
});
307320

packages/docsearch-react/src/__tests__/imperative.test.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,26 @@ import '@testing-library/jest-dom/vitest';
77

88
import { type DocSearchProps, DocSearch as DocSearchComponent, type DocSearchRef } from '../DocSearch';
99

10+
const typesenseServerConfig = {
11+
apiKey: 'test-key',
12+
nodes: [{ host: 'localhost', port: 8108, protocol: 'http' as const }],
13+
};
14+
1015
type TestDocSearchProps = DocSearchCallbacks & Partial<DocSearchProps> & { refObj?: RefObject<DocSearchRef | null> };
1116

1217
function DocSearch(props: TestDocSearchProps): JSX.Element {
1318
const internalRef = useRef<DocSearchRef>(null);
1419
const ref = props.refObj ?? internalRef;
1520

16-
return <DocSearchComponent ref={ref} appId="woo" apiKey="foo" indexName="bar" {...props} />;
21+
return (
22+
<DocSearchComponent
23+
ref={ref}
24+
typesenseCollectionName="docs"
25+
typesenseServerConfig={typesenseServerConfig}
26+
typesenseSearchParameters={{}}
27+
{...props}
28+
/>
29+
);
1730
}
1831

1932
describe('imperative handle', () => {

packages/docsearch-react/src/__tests__/keyboardShortcuts.test.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,20 @@ import '@testing-library/jest-dom/vitest';
77
import { DocSearch as DocSearchComponent } from '../DocSearch';
88
import type { DocSearchProps } from '../DocSearch';
99

10+
const typesenseServerConfig = {
11+
apiKey: 'test-key',
12+
nodes: [{ host: 'localhost', port: 8108, protocol: 'http' as const }],
13+
};
14+
1015
function DocSearch(props: Partial<DocSearchProps>): JSX.Element {
11-
return <DocSearchComponent appId="woo" apiKey="foo" indexName="bar" {...props} />;
16+
return (
17+
<DocSearchComponent
18+
typesenseCollectionName="docs"
19+
typesenseServerConfig={typesenseServerConfig}
20+
typesenseSearchParameters={{}}
21+
{...props}
22+
/>
23+
);
1224
}
1325

1426
describe('keyboard shortcuts', () => {

0 commit comments

Comments
 (0)