Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@rollup/plugin-terser": "0.4.4",
"@stylistic/eslint-plugin": "2.13.0",
"@testing-library/dom": "10.4.0",
"@testing-library/react": "^16.3.2",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@typescript-eslint/eslint-plugin": "8.20.0",
Expand Down
7 changes: 7 additions & 0 deletions packages/docsearch-core/src/DocSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface DocSearchContext {
isModalActive: boolean;
onAskAiToggle: OnAskAiToggle;
initialAskAiMessage: InitialAskAiMessage | undefined;
clearInitialAskAiMessage: () => void;
registerView: (view: View) => void;
isHybridModeSupported: boolean;
}
Expand Down Expand Up @@ -183,6 +184,10 @@ function DocSearchInner(
[setDocsearchState, setInitialQuery],
);

const clearInitialAskAiMessage = React.useCallback((): void => {
setInitialAskAiMessage(undefined);
}, []);

const registerView = React.useCallback(
(view: View): void => {
if (registeredViews.has(view)) return;
Expand Down Expand Up @@ -246,6 +251,7 @@ function DocSearchInner(
isModalActive,
onAskAiToggle,
initialAskAiMessage,
clearInitialAskAiMessage,
registerView,
isHybridModeSupported,
}),
Expand All @@ -260,6 +266,7 @@ function DocSearchInner(
isModalActive,
onAskAiToggle,
initialAskAiMessage,
clearInitialAskAiMessage,
registerView,
isHybridModeSupported,
],
Expand Down
38 changes: 38 additions & 0 deletions packages/docsearch-css/src/sidepanel.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@
--docsearch-sidepanel-hit-highlight-color: var(--docsearch-hit-highlight-color);
--docsearch-sidepanel-button-background: var(--docsearch-sidepanel-background);
--docsearch-sidepanel-button-background-dark: var(--docsearch-sidepanel-background);
--docsearch-sidepanel-thread-depth-banner-bg: #fff1f2;
--docsearch-sidepanel-thread-depth-banner-border: #fecdd3;
}

html[data-theme='dark'] {
--docsearch-sidepanel-text-base: var(--docsearch-text-color);
--docsearch-sidepanel-primary-disabled: rgba(1, 45, 186, 0.60);
--docsearch-sidepanel-button-background-dark: rgba(4, 4, 8, 1);
--docsearch-sidepanel-thread-depth-banner-bg: rgb(239 83 80 / 12%);
--docsearch-sidepanel-thread-depth-banner-border: rgb(254 205 211 / 35%);
}

.DocSearch-SidepanelButton {
Expand Down Expand Up @@ -623,6 +627,40 @@ html[data-theme="dark"] .DocSearch-Sidepanel-Prompt--stop:hover {
flex-direction: column;
}

@keyframes docsearch-sidepanel-thread-depth-banner-enter {
from {
opacity: 0;
transform: translateY(-10px);
}

to {
opacity: 1;
transform: translateY(0);
}
}

.DocSearch-Sidepanel .DocSearch-Sidepanel-ThreadDepthBanner {
display: flex;
flex-direction: column;
margin: 0;
flex-shrink: 0;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
background-color: var(--docsearch-sidepanel-thread-depth-banner-bg);
border: 1px solid var(--docsearch-sidepanel-thread-depth-banner-border);
box-sizing: border-box;
color: var(--docsearch-sidepanel-text-base);
font-size: 0.75rem;
line-height: 1rem;
font-weight: 400;
width: 100%;
animation: docsearch-sidepanel-thread-depth-banner-enter 0.3s ease-out;
}

.DocSearch-Sidepanel .DocSearch-Sidepanel-ThreadDepthBanner p {
margin: 0;
}

@media screen and (min-width: 769px) {
.DocSearch-Sidepanel-ConversationScreen {
padding: 1rem 0;
Expand Down
19 changes: 7 additions & 12 deletions packages/docsearch-react/src/AskAiScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import type { StoredSearchPlugin } from './stored-searches';
import { ToolCall } from './ToolCall';
import type { InternalDocSearchHit, StoredAskAiState } from './types';
import type { AIMessage } from './types/AskiAi';
import { extractLinksFromMessage, getMessageContent, isThreadDepthError } from './utils/ai';
import {
extractLinksFromMessage,
filterExchangesForThreadDepthError,
getMessageContent,
isThreadDepthError,
} from './utils/ai';
import { groupConsecutiveToolResults } from './utils/groupConsecutiveToolResults';

export type AskAiScreenTranslations = Partial<{
Expand Down Expand Up @@ -392,17 +397,7 @@ export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps):
}
}

// If there's a thread depth error, remove the last exchange (the one that triggered the error)
// We only want to show successful exchanges
if (hasThreadDepthError && grouped.length > 0) {
// Check if the last exchange has no assistant message (failed to complete)
const lastExchange = grouped[grouped.length - 1];
if (!lastExchange.assistantMessage) {
grouped.pop();
}
}

return grouped;
return filterExchangesForThreadDepthError(grouped, hasThreadDepthError);
}, [messages, hasThreadDepthError]);

const handleSearchQueryClick = (query: string): void => {
Expand Down
79 changes: 57 additions & 22 deletions packages/docsearch-react/src/DocSearchModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -398,16 +398,26 @@ export function DocSearchModal({

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

const { messages, status, setMessages, sendMessage, stopAskAiStreaming, askAiError, sendFeedback, conversations } =
useAskAi({
assistantId: askAiConfigurationId,
apiKey: askAiConfig?.apiKey || apiKey,
appId: askAiConfig?.appId || appId,
indexName: askAiConfig?.indexName || defaultIndexName,
searchParameters: askAiSearchParameters,
useStagingEnv: askAiUseStagingEnv,
agentStudio,
});
const {
messages,
status,
sendMessage,
stopAskAiStreaming,
askAiError,
sendFeedback,
conversations,
clearError,
resetAskAiAbortScope,
resetAskAiChatSession,
} = useAskAi({
assistantId: askAiConfigurationId,
apiKey: askAiConfig?.apiKey || apiKey,
appId: askAiConfig?.appId || appId,
indexName: askAiConfig?.indexName || defaultIndexName,
searchParameters: askAiSearchParameters,
useStagingEnv: askAiUseStagingEnv,
agentStudio,
});

const prevStatus = React.useRef(status);
React.useEffect(() => {
Expand All @@ -423,9 +433,12 @@ export function DocSearchModal({
};
}

for (const part of messages[0].parts) {
if (part.type === 'text') {
conversations.add(buildDummyAskAiHit(part.text, messages));
const first = messages[0];
if (first?.parts) {
for (const part of first.parts) {
if (part.type === 'text') {
conversations.add(buildDummyAskAiHit(part.text, messages));
}
}
}
}
Expand Down Expand Up @@ -533,6 +546,8 @@ export function DocSearchModal({
if (isHybridModeSupported) return;

setStoppedStream(false);
resetAskAiAbortScope();
clearError();

const messageOptions: ChatRequestOptions = {};

Expand Down Expand Up @@ -571,7 +586,16 @@ export function DocSearchModal({
autocompleteRef.current.setQuery('');
}
},
[onAskAiToggle, interceptAskAiEvent, sendMessage, askAiState, setAskAiState, isHybridModeSupported],
[
onAskAiToggle,
interceptAskAiEvent,
askAiState,
setAskAiState,
isHybridModeSupported,
clearError,
resetAskAiAbortScope,
sendMessage,
],
);

// feedback handler
Expand Down Expand Up @@ -623,7 +647,10 @@ export function DocSearchModal({
},
onSelect({ item }): void {
if (item.messages) {
setMessages(item.messages as any);
resetAskAiChatSession({
kind: 'setMessages',
messages: item.messages as AIMessage[],
});
onAskAiToggle(true);
}
},
Expand Down Expand Up @@ -793,14 +820,18 @@ export function DocSearchModal({
};
}, []);

// Refresh the autocomplete results when ask ai is toggled off
// helps return to the previous ac state and start screen
// Refresh autocomplete and rotate the chat session only when Ask AI is turned off — not on every
// mount while inactive. `clearError` from `useChat` can change when `chatSessionId` changes; listing
// it (and re-running `resetAskAiChatSession` on that) caused an infinite update loop.
const prevIsAskAiActiveRef = React.useRef(isAskAiActive);
React.useEffect(() => {
if (!isAskAiActive) {
if (prevIsAskAiActiveRef.current && !isAskAiActive) {
autocomplete.refresh();
setMessages([]);
clearError();
resetAskAiChatSession();
}
}, [isAskAiActive, autocomplete, setMessages]);
prevIsAskAiActiveRef.current = isAskAiActive;
}, [isAskAiActive, autocomplete, clearError, resetAskAiChatSession]);

// Track external state in order to manage internal askAiState
React.useEffect(() => {
Expand All @@ -814,7 +845,8 @@ export function DocSearchModal({
};

const handleNewConversation = (): void => {
setMessages([]);
clearError();
resetAskAiChatSession();
setAskAiState('new-conversation');
};

Expand Down Expand Up @@ -913,7 +945,10 @@ export function DocSearchModal({
if (item.type === 'askAI' && item.query) {
// if the item is askAI and the anchor is stored
if (item.anchor === 'stored' && 'messages' in item) {
setMessages(item.messages as any);
resetAskAiChatSession({
kind: 'setMessages',
messages: item.messages as AIMessage[],
});
const initialMessage: InitialAskAiMessage = {
query: item.query,
messageId: (item.messages as StoredAskAiMessage[])[0].id,
Expand Down
10 changes: 9 additions & 1 deletion packages/docsearch-react/src/Sidepanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,21 @@ function DocSearchSidepanelComp({
panel: { portalContainer, ...panelProps } = {},
...rootProps
}: DocSearchSidepanelProps): JSX.Element {
const { docsearchState, setDocsearchState, keyboardShortcuts, registerView, initialAskAiMessage } = useDocSearch();
const {
docsearchState,
setDocsearchState,
keyboardShortcuts,
registerView,
initialAskAiMessage,
clearInitialAskAiMessage,
} = useDocSearch();

const toggleSidepanelState = React.useCallback(() => {
setDocsearchState(docsearchState === 'sidepanel' ? 'ready' : 'sidepanel');
}, [docsearchState, setDocsearchState]);

const handleClose = (): void => {
clearInitialAskAiMessage();
setDocsearchState('ready');
};

Expand Down
16 changes: 13 additions & 3 deletions packages/docsearch-react/src/Sidepanel/ConversationScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { StoredSearchPlugin } from '../stored-searches';
import { ToolCall, type ToolCallTranslations } from '../ToolCall';
import type { StoredAskAiState } from '../types';
import { isAIToolPart, type AIMessage } from '../types/AskiAi';
import { extractLinksFromMessage, getMessageContent } from '../utils/ai';
import { extractLinksFromMessage, getMessageContent, isThreadDepthError } from '../utils/ai';
import { groupConsecutiveToolResults } from '../utils/groupConsecutiveToolResults';

import { AggregatedSearchBlock } from './AggregatedSearchBlock';
Expand Down Expand Up @@ -60,7 +60,15 @@ export type ConversationScreenTranslations = Partial<
/**
* Error title shown if there is an error while chatting.
*/
errorTitleText;
errorTitleText: string;
/**
* Message shown when thread depth limit is exceeded (AI-217).
*/
threadDepthExceededMessage: string;
/**
* Button label to start a new conversation after a thread depth error.
*/
startNewConversationButtonText: string;
}
>;

Expand Down Expand Up @@ -105,6 +113,8 @@ const ConversationExchange = React.forwardRef<HTMLDivElement, ConversationnExcha
errorTitleText = 'Chat error',
} = translations;

const isThreadDepth = isThreadDepthError(streamError);

const assistantContent = useMemo(() => getMessageContent(assistantMessage), [assistantMessage]);
const userContent = useMemo(() => getMessageContent(userMessage), [userMessage]);

Expand All @@ -130,7 +140,7 @@ const ConversationExchange = React.forwardRef<HTMLDivElement, ConversationnExcha
</div>
<div className="DocSearch-AskAiScreen-Message DocSearch-AskAiScreen-Message--assistant">
<div className="DocSearch-AskAiScreen-MessageContent">
{status === 'error' && streamError && isLastExchange && (
{status === 'error' && streamError && isLastExchange && !isThreadDepth && (
<div className="DocSearch-AskAiScreen-MessageContent DocSearch-AskAiScreen-Error">
<AlertIcon />
<div className="DocSearch-AskAiScreen-Error-Content">
Expand Down
Loading