Skip to content

Commit fd0de3d

Browse files
feat(askai): add thread depth limit logic (#2812)
1 parent 8a9c451 commit fd0de3d

File tree

8 files changed

+146
-13
lines changed

8 files changed

+146
-13
lines changed

bundlesize.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
},
1919
{
2020
"path": "packages/docsearch-modal/dist/umd/index.js",
21-
"maxSize": "113 kB"
21+
"maxSize": "113.5 kB"
2222
}
2323
]
2424
}

packages/docsearch-css/src/modal.css

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,46 @@ assistive tech users */
919919
color: var(--docsearch-error-color);
920920
}
921921

922+
/* Thread Depth Error - positioned below input */
923+
.DocSearch-AskAiScreen-Error--ThreadDepth {
924+
margin: 12px 0 8px 0;
925+
color: var(--docsearch-text-color);
926+
font-size: 12px;
927+
border: 1px solid #febdc5;
928+
animation: slideDown 0.3s ease-out;
929+
width: 100%;
930+
}
931+
932+
@keyframes slideDown {
933+
from {
934+
opacity: 0;
935+
transform: translateY(-10px);
936+
}
937+
to {
938+
opacity: 1;
939+
transform: translateY(0);
940+
}
941+
}
942+
943+
.DocSearch-ThreadDepthError-Link {
944+
color: var(--docsearch-highlight-color);
945+
text-decoration: underline;
946+
cursor: pointer;
947+
background: none;
948+
border: none;
949+
padding: 0;
950+
font-size: inherit;
951+
font-family: inherit;
952+
}
953+
954+
.DocSearch-ThreadDepthError-Link:hover {
955+
opacity: 0.8;
956+
}
957+
958+
.DocSearch-ThreadDepthError-Link:active {
959+
color: rgb(153 27 27);
960+
}
961+
922962
.DocSearch-AskAiScreen-FeedbackText {
923963
font-size: 0.7em;
924964
font-weight: 400;
@@ -1457,7 +1497,9 @@ assistive tech users */
14571497
flex-direction: column;
14581498
border-radius: var(--docsearch-border-radius);
14591499
background-color: var(--docsearch-dropdown-menu-background);
1460-
box-shadow: 0px 0px 0px 1px #21243D0D, 0px 8px 16px -4px #21243D40;
1500+
box-shadow:
1501+
0px 0px 0px 1px #21243d0d,
1502+
0px 8px 16px -4px #21243d40;
14611503
min-width: 195px;
14621504
inset-block-start: calc(100% + 12px);
14631505
z-index: 422;

packages/docsearch-react/src/AskAiScreen.tsx

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { ScreenStateProps } from './ScreenState';
88
import type { StoredSearchPlugin } from './stored-searches';
99
import type { InternalDocSearchHit, StoredAskAiState } from './types';
1010
import type { AIMessage } from './types/AskiAi';
11-
import { extractLinksFromMessage, getMessageContent } from './utils/ai';
11+
import { extractLinksFromMessage, getMessageContent, isThreadDepthError } from './utils/ai';
1212
import { groupConsecutiveToolResults } from './utils/groupConsecutiveToolResults';
1313

1414
export type AskAiScreenTranslations = Partial<{
@@ -52,13 +52,22 @@ export type AskAiScreenTranslations = Partial<{
5252
* Error title shown if there is an error while chatting.
5353
*/
5454
errorTitleText: string;
55+
/**
56+
* Message shown when thread depth limit is exceeded (AI-217 error).
57+
*/
58+
threadDepthExceededMessage: string;
59+
/**
60+
* Button text for starting a new conversation after thread depth error.
61+
*/
62+
startNewConversationButtonText: string;
5563
}>;
5664

5765
type AskAiScreenProps = Omit<ScreenStateProps<InternalDocSearchHit>, 'translations'> & {
5866
messages: AIMessage[];
5967
status: UseChatHelpers<AIMessage>['status'];
6068
askAiError?: Error;
6169
translations?: AskAiScreenTranslations;
70+
onNewConversation: () => void;
6271
};
6372

6473
interface AskAiScreenHeaderProps {
@@ -100,6 +109,8 @@ function AskAiExchangeCard({
100109

101110
const { stoppedStreamingText = 'You stopped this response', errorTitleText = 'Chat error' } = translations;
102111

112+
const isThreadDepth = isThreadDepthError(askAiError);
113+
103114
const assistantContent = useMemo(() => getMessageContent(assistantMessage), [assistantMessage]);
104115
const userContent = useMemo(() => getMessageContent(userMessage), [userMessage]);
105116

@@ -127,7 +138,7 @@ function AskAiExchangeCard({
127138
</div>
128139
<div className="DocSearch-AskAiScreen-Message DocSearch-AskAiScreen-Message--assistant">
129140
<div className="DocSearch-AskAiScreen-MessageContent">
130-
{loadingStatus === 'error' && askAiError && isLastExchange && (
141+
{loadingStatus === 'error' && askAiError && isLastExchange && !isThreadDepth && (
131142
<div className="DocSearch-AskAiScreen-MessageContent DocSearch-AskAiScreen-Error">
132143
<AlertIcon />
133144
<div className="DocSearch-AskAiScreen-Error-Content">
@@ -375,9 +386,18 @@ function AskAiSourcesPanel({ urlsToDisplay, relatedSourcesText }: AskAiSourcesPa
375386
}
376387

377388
export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps): JSX.Element | null {
378-
const { disclaimerText = 'Answers are generated with AI which can make mistakes. Verify responses.' } = translations;
389+
const {
390+
disclaimerText = 'Answers are generated with AI which can make mistakes. Verify responses.',
391+
threadDepthExceededMessage = 'This conversation is now closed to keep responses accurate.',
392+
startNewConversationButtonText = 'Start a new conversation',
393+
} = translations;
379394

380-
const { messages } = props;
395+
const { messages, askAiError, status } = props;
396+
397+
// Check if there's a thread depth error
398+
const hasThreadDepthError = useMemo(() => {
399+
return status === 'error' && isThreadDepthError(askAiError);
400+
}, [status, askAiError]);
381401

382402
// Group messages into exchanges (user + assistant pairs)
383403
const exchanges: Exchange[] = useMemo(() => {
@@ -392,17 +412,47 @@ export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps):
392412
}
393413
}
394414
}
415+
416+
// If there's a thread depth error, remove the last exchange (the one that triggered the error)
417+
// We only want to show successful exchanges
418+
if (hasThreadDepthError && grouped.length > 0) {
419+
// Check if the last exchange has no assistant message (failed to complete)
420+
const lastExchange = grouped[grouped.length - 1];
421+
if (!lastExchange.assistantMessage) {
422+
grouped.pop();
423+
}
424+
}
425+
395426
return grouped;
396-
}, [messages]);
427+
}, [messages, hasThreadDepthError]);
397428

398429
const handleSearchQueryClick = (query: string): void => {
399430
props.onAskAiToggle(false);
400431
props.setQuery(query);
401432
};
402433

434+
// Only show the thread depth error if we have assistant messages
435+
const showThreadDepthError = hasThreadDepthError && messages.some((m) => m.role === 'assistant');
436+
403437
return (
404438
<div className="DocSearch-AskAiScreen DocSearch-AskAiScreen-Container">
439+
{/* Thread Depth Error */}
440+
{showThreadDepthError && (
441+
<div className="DocSearch-AskAiScreen-MessageContent DocSearch-AskAiScreen-Error DocSearch-AskAiScreen-Error--ThreadDepth">
442+
<div className="DocSearch-AskAiScreen-Error-Content">
443+
<p>
444+
{threadDepthExceededMessage}{' '}
445+
<button type="button" className="DocSearch-ThreadDepthError-Link" onClick={props.onNewConversation}>
446+
{startNewConversationButtonText}
447+
</button>{' '}
448+
to continue.
449+
</p>
450+
</div>
451+
</div>
452+
)}
453+
405454
<AskAiScreenHeader disclaimerText={disclaimerText} />
455+
406456
<div className="DocSearch-AskAiScreen-Body">
407457
<div className="DocSearch-AskAiScreen-ExchangesList">
408458
{exchanges

packages/docsearch-react/src/DocSearchModal.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { useSuggestedQuestions } from './useSuggestedQuestions';
3737
import { useTouchEvents } from './useTouchEvents';
3838
import { useTrapFocus } from './useTrapFocus';
3939
import { groupBy, identity, noop, removeHighlightTags, isModifierEvent, scrollTo as scrollToUtils } from './utils';
40-
import { buildDummyAskAiHit } from './utils/ai';
40+
import { buildDummyAskAiHit, isThreadDepthError } from './utils/ai';
4141
import { manageLocalStorageQuota } from './utils/storage';
4242

4343
export type ModalTranslations = Partial<{
@@ -459,6 +459,11 @@ export function DocSearchModal({
459459
prevStatus.current = status;
460460
}, [status, messages, conversations, disableUserPersonalization, stoppedStream]);
461461

462+
// Check if there's a thread depth error (AI-217)
463+
const hasThreadDepthError = React.useMemo(() => {
464+
return status === 'error' && isThreadDepthError(askAiError as Error | undefined);
465+
}, [status, askAiError]);
466+
462467
const createSyntheticParent = React.useCallback(function createSyntheticParent(
463468
item: InternalDocSearchHit,
464469
): InternalDocSearchHit {
@@ -527,8 +532,6 @@ export function DocSearchModal({
527532
const handleSelectAskAiQuestion = React.useCallback(
528533
(toggle: boolean, query: string, suggestedQuestion: SuggestedQuestionHit | undefined = undefined) => {
529534
if (toggle && askAiState === 'new-conversation') {
530-
// We're starting a new conversation, clear out current messages
531-
setMessages([]);
532535
setAskAiState('initial');
533536
}
534537

@@ -571,7 +574,7 @@ export function DocSearchModal({
571574
autocompleteRef.current.setQuery('');
572575
}
573576
},
574-
[onAskAiToggle, sendMessage, askAiState, setAskAiState, setMessages],
577+
[onAskAiToggle, sendMessage, askAiState, setAskAiState],
575578
);
576579

577580
// feedback handler
@@ -821,6 +824,7 @@ export function DocSearchModal({
821824
};
822825

823826
const handleNewConversation = (): void => {
827+
setMessages([]);
824828
setAskAiState('new-conversation');
825829
};
826830

@@ -871,8 +875,10 @@ export function DocSearchModal({
871875
translations={searchBoxTranslations}
872876
isAskAiActive={isAskAiActive}
873877
askAiStatus={status}
878+
askAiError={askAiError}
874879
askAiState={askAiState}
875880
setAskAiState={setAskAiState}
881+
isThreadDepthError={hasThreadDepthError && askAiState !== 'new-conversation'}
876882
onClose={onClose}
877883
onAskAiToggle={onAskAiToggle}
878884
onAskAgain={(query) => {
@@ -910,6 +916,7 @@ export function DocSearchModal({
910916
suggestedQuestions={suggestedQuestions}
911917
selectSuggestedQuestion={selectSuggestedQuestion}
912918
onAskAiToggle={onAskAiToggle}
919+
onNewConversation={handleNewConversation}
913920
onItemClick={(item, event) => {
914921
// if the item is askAI toggle the screen
915922
if (item.type === 'askAI' && item.query) {

packages/docsearch-react/src/ScreenState.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export interface ScreenStateProps<TItem extends BaseItem>
5555
selectAskAiQuestion: (toggle: boolean, query: string) => void;
5656
suggestedQuestions: SuggestedQuestionHit[];
5757
selectSuggestedQuestion: (question: SuggestedQuestionHit) => void;
58+
onNewConversation: () => void;
5859
}
5960

6061
export const ScreenState = React.memo(

packages/docsearch-react/src/SearchBox.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export type SearchBoxTranslations = Partial<{
3535
conversationHistoryTitle: string;
3636
startNewConversationText: string;
3737
viewConversationHistoryText: string;
38+
threadDepthErrorPlaceholder: string;
3839
}>;
3940

4041
interface SearchBoxProps
@@ -49,12 +50,14 @@ interface SearchBoxProps
4950
placeholder: string;
5051
isAskAiActive: boolean;
5152
askAiStatus: UseChatHelpers<AIMessage>['status'];
53+
askAiError?: Error;
5254
isFromSelection: boolean;
5355
translations?: SearchBoxTranslations;
5456
askAiState: AskAiState;
5557
setAskAiState: (state: AskAiState) => void;
5658
onNewConversation: () => void;
5759
onViewConversationHistory: () => void;
60+
isThreadDepthError?: boolean;
5861
}
5962

6063
export function SearchBox({
@@ -77,6 +80,7 @@ export function SearchBox({
7780
conversationHistoryTitle = 'My conversation history',
7881
startNewConversationText = 'Start a new conversation',
7982
viewConversationHistoryText = 'Conversation history',
83+
threadDepthErrorPlaceholder = 'Conversation limit reached',
8084
} = translations;
8185
const { onReset } = props.getFormProps({
8286
inputElement: props.inputRef.current,
@@ -116,12 +120,20 @@ export function SearchBox({
116120
const isAskAiStreaming = props.askAiStatus === 'streaming' || props.askAiStatus === 'submitted';
117121
const isKeywordSearchLoading = props.state.status === 'stalled';
118122
const renderMoreOptions = props.isAskAiActive && askAiState !== 'conversation-history';
123+
124+
// Use the thread depth error state passed from parent
125+
const isThreadDepthError = props.isThreadDepthError || false;
119126
let searchPlaceholder = props.placeholder;
120127

121128
if (askAiState === 'new-conversation') {
122129
searchPlaceholder = newConversationPlaceholder;
123130
}
124131

132+
// Override placeholder when thread depth error occurs (only in Ask AI mode)
133+
if (isThreadDepthError && props.isAskAiActive) {
134+
searchPlaceholder = threadDepthErrorPlaceholder;
135+
}
136+
125137
let heading: string | null = null;
126138

127139
if (isAskAiStreaming) {
@@ -174,18 +186,24 @@ export function SearchBox({
174186
}
175187
origOnChange?.(e);
176188
},
177-
disabled: isAskAiStreaming,
189+
disabled: isAskAiStreaming || (isThreadDepthError && props.isAskAiActive),
178190
};
179191

180192
const handleAskAiBackClick = React.useCallback((): void => {
193+
// If there's a thread depth error, start a new conversation instead of exiting
194+
if (isThreadDepthError) {
195+
props.onNewConversation();
196+
return;
197+
}
198+
181199
if (askAiState === 'conversation-history') {
182200
onAskAiToggle(true);
183201
setAskAiState('initial');
184202
return;
185203
}
186204

187205
onAskAiToggle(false);
188-
}, [askAiState, onAskAiToggle, setAskAiState]);
206+
}, [askAiState, isThreadDepthError, onAskAiToggle, setAskAiState, props]);
189207

190208
return (
191209
<>

packages/docsearch-react/src/utils/ai.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,12 @@ export const buildDummyAskAiHit = (query: string, messages: AIMessage[]): Stored
9595

9696
export const getMessageContent = (message: AIMessage | null): TextUIPart | undefined =>
9797
message?.parts.find((part) => part.type === 'text');
98+
99+
/**
100+
* Helper function to check if error is a thread depth error (AI-217).
101+
*/
102+
export function isThreadDepthError(error?: Error): boolean {
103+
if (!error) return false;
104+
105+
return error.message?.includes('AI-217') || false;
106+
}

packages/website/docs/v4/askai-errors.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ This error occurs when the SDK could not load the provider’s API key successfu
110110

111111
> **Solution**: Ensure the API key for the chosen LLM provider is correct and has all of the correct permissions added to it.
112112
113+
### AI-217 - Thread Depth Exceeded {#ai-217}
114+
115+
The conversation has reached its maximum depth limit and can no longer accept follow-up questions.
116+
117+
> **Solution**: Start a new conversation to continue. The conversation depth limit is configured per assistant to maintain response accuracy and prevent conversation drift.
118+
113119
[1]: /docs/api#askai
114120
[2]: https://sitesearch.algolia.com/docs/experiences/search-askai#configuration
115121
[3]: https://www.algolia.com/doc/guides/algolia-ai/askai/reference/api

0 commit comments

Comments
 (0)