Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 13 additions & 3 deletions packages/docsearch-css/src/modal.css
Original file line number Diff line number Diff line change
Expand Up @@ -87,20 +87,25 @@
background: transparent;
border: 0;
color: var(--docsearch-text-color);
flex: 1;
flex: 1 1 0%;
min-width: 0;
font: inherit;
font-size: 1.2em;
font-weight: 300;
height: 100%;
outline: none;
padding-block-start: 0px;
padding-inline-start: 8px;
width: 80%;
line-height: 1.4;
resize: none;
overflow-y: hidden; /* js toggles to auto when exceeding max */
}

.DocSearch-Input {
overflow-x: hidden;
text-overflow: ellipsis;
}

.DocSearch-Input::placeholder {
color: var(--docsearch-muted-color);
opacity: 1; /* Firefox */
Expand All @@ -114,7 +119,8 @@
}

.DocSearch-Actions {
width: var(--docsearch-actions-width);
flex: 0 0 auto;
width: auto;
height: var(--docsearch-actions-height);
display: flex;
align-items: center;
Expand Down Expand Up @@ -929,6 +935,10 @@ assistive tech users */
width: 100%;
}

.DocSearch-AskAiScreen-Error--ThreadDepth .DocSearch-AskAiScreen-Error-Title {
margin-bottom: 6px;
}

@keyframes slideDown {
from {
opacity: 0;
Expand Down
43 changes: 43 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,45 @@ 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;
}

.DocSearch-Sidepanel .DocSearch-Sidepanel-ThreadDepthBanner-apiMessage {
font-weight: 700;
margin-bottom: 0.5rem;
}

@media screen and (min-width: 769px) {
.DocSearch-Sidepanel-ConversationScreen {
padding: 1rem 0;
Expand Down
25 changes: 13 additions & 12 deletions packages/docsearch-react/src/AskAiScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ 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,
getThreadDepthErrorUserFacingMessage,
isThreadDepthError,
} from './utils/ai';
import { groupConsecutiveToolResults } from './utils/groupConsecutiveToolResults';

export type AskAiScreenTranslations = Partial<{
Expand Down Expand Up @@ -378,6 +384,8 @@ export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps):
return status === 'error' && isThreadDepthError(askAiError);
}, [status, askAiError]);

const threadDepthApiMessage = useMemo(() => getThreadDepthErrorUserFacingMessage(askAiError), [askAiError]);

// Group messages into exchanges (user + assistant pairs)
const exchanges: Exchange[] = useMemo(() => {
const grouped: Exchange[] = [];
Expand All @@ -392,17 +400,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 All @@ -419,6 +417,9 @@ export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps):
{showThreadDepthError && (
<div className="DocSearch-AskAiScreen-MessageContent DocSearch-AskAiScreen-Error DocSearch-AskAiScreen-Error--ThreadDepth">
<div className="DocSearch-AskAiScreen-Error-Content">
{threadDepthApiMessage ? (
<p className="DocSearch-AskAiScreen-Error-Title">{threadDepthApiMessage}</p>
) : null}
<p>
{threadDepthExceededMessage}{' '}
<button type="button" className="DocSearch-ThreadDepthError-Link" onClick={props.onNewConversation}>
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
Loading