1
0
Fork 0

fix(askai): Ask AI fixes for v5 (#2945)

This commit is contained in:
Paul Jankowski 2026-08-03 12:22:45 -04:00 committed by GitHub
parent b87c28fc87
commit 9fe6738a35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 217 additions and 111 deletions

View file

@ -0,0 +1,20 @@
---
"@docsearch/react": patch
"@docsearch/css": patch
---
fix(askai): Ask AI fixes for v5
- `getMessageContent` now joins every assistant text part instead of stopping
at the first one, so copying an answer that interleaves text with tool calls
returns the complete response (#2782)
- Key Ask AI conversation storage by `appId` instead of `indexName`, fixes
triggering a new conversation in hybrid mode
- Negative feedback panel is labelled with `aria-labelledby` and reason chips
expose selection via `aria-pressed` (styling moved off the
`--selected` modifier)
- `Popover.Trigger` forwards refs, fixes sources panel not showing in some cases
- Add `--docsearch-error-soft-color`, `--docsearch-error-text-color`, and
`--docsearch-code-block-background` variables; restyle the Ask AI error panel
and markdown code blocks to use them
```

View file

@ -317,7 +317,8 @@
"typescript/prefer-namespace-keyword": ["error"], "typescript/prefer-namespace-keyword": ["error"],
"typescript/prefer-ts-expect-error": ["error"], "typescript/prefer-ts-expect-error": ["error"],
"typescript/unified-signatures": ["error"], "typescript/unified-signatures": ["error"],
"vitest/require-mock-type-parameters": "off" "vitest/require-mock-type-parameters": "off",
"no-debugger": ["error"]
}, },
"overrides": [ "overrides": [
{ {

View file

@ -78,27 +78,6 @@ bun run pw:run:webkit
## Code Style Guidelines ## Code Style Guidelines
### Imports
Imported modules must be ordered alphabetically with newlines between groups:
1. Built-in modules
2. External dependencies
3. Parent directory imports
4. Sibling imports
5. Index imports
Internal `@/**/*` paths go before parent imports.
```typescript
// Correct order
import type { AutocompleteOptions } from '@algolia/autocomplete-core';
import React, { type JSX } from 'react';
import { DocSearchButton } from './DocSearchButton';
import type { DocSearchHit } from './types';
```
### TypeScript ### TypeScript
- Use `type` imports for type-only imports: `import type { Foo } from './types'` - Use `type` imports for type-only imports: `import type { Foo } from './types'`

View file

@ -1,6 +1,5 @@
/* eslint-disable react/react-in-jsx-scope */ /* eslint-disable react/react-in-jsx-scope */
import { version } from '@docsearch/react'; import { version } from '@docsearch/react';
import { DocSearchSidepanel } from '@docsearch/react/sidepanel';
import { useState, type JSX } from 'react'; import { useState, type JSX } from 'react';
import './App.css'; import './App.css';
@ -121,17 +120,6 @@ function App(): JSX.Element {
</section> </section>
</main> </main>
</div> </div>
<DocSearchSidepanel
assistantId="ccdec697-e3fe-465b-a1c3-657e7bf18aef"
indexName="docsearch"
appId="PMZUYBQDAK"
apiKey="24b09689d5b4223813d9b8e48563c8f6"
panel={{
suggestedQuestions: true,
}}
theme={theme}
/>
</div> </div>
); );
} }

View file

@ -201,19 +201,12 @@
.DocSearch-AskAiScreen-Answer-Footer { .DocSearch-AskAiScreen-Answer-Footer {
display: flex; display: flex;
flex-direction: row; flex-flow: row wrap;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 8px; gap: 8px;
} }
.DocSearch-AskAiScreen-Actions {
display: flex;
flex-direction: column;
width: 100%;
gap: var(--docsearch-spacing);
}
.DocSearch-AskAiScreen-Actions-Controls { .DocSearch-AskAiScreen-Actions-Controls {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -226,13 +219,16 @@
.DocSearch-Feedback-Panel { .DocSearch-Feedback-Panel {
flex-basis: 100%; flex-basis: 100%;
width: 100%; width: 100%;
padding: var(--docsearch-spacing); padding-block: var(--docsearch-spacing);
padding-inline: var(--docsearch-spacing);
margin-block-start: var(--docsearch-spacing);
border-top: 1px solid var(--docsearch-subtle-color); border-top: 1px solid var(--docsearch-subtle-color);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 0.75rem;
text-align: start; text-align: start;
animation: fade-in 0.2s ease-in forwards; animation: fade-in 0.2s ease-in forwards;
font-size: 0.875rem;
} }
.DocSearch-Feedback-Panel-Header { .DocSearch-Feedback-Panel-Header {
@ -253,7 +249,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 4px; padding-inline: 0.25rem;
padding-block: 0.25rem;
border: 0; border: 0;
background: none; background: none;
cursor: pointer; cursor: pointer;
@ -267,8 +264,8 @@
} }
.DocSearch-Feedback-Panel-Close svg { .DocSearch-Feedback-Panel-Close svg {
width: 16px; width: 1rem;
height: 16px; height: 1rem;
} }
.DocSearch-Feedback-Panel-Reasons { .DocSearch-Feedback-Panel-Reasons {
@ -280,8 +277,9 @@
.DocSearch-Feedback-Panel-Reason { .DocSearch-Feedback-Panel-Reason {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 0.375rem;
padding: 6px 12px; padding-block: 0.375rem;
padding-inline: 0.75rem;
border-radius: var(--docsearch-border-radius); border-radius: var(--docsearch-border-radius);
border: 1px solid var(--docsearch-subtle-color); border: 1px solid var(--docsearch-subtle-color);
background: var(--docsearch-hit-background); background: var(--docsearch-hit-background);
@ -293,18 +291,13 @@
border-color 0.2s ease, border-color 0.2s ease,
background-color 0.2s ease, background-color 0.2s ease,
color 0.2s ease; color 0.2s ease;
}
.DocSearch-Feedback-Panel-Reason:hover { &:hover,
border-color: var(--docsearch-highlight-color); &[aria-pressed='true'] {
background: var(--docsearch-hit-highlight-color); border-color: var(--docsearch-highlight-color);
} background: var(--docsearch-hit-highlight-color);
color: var(--docsearch-highlight-color);
.DocSearch-Feedback-Panel-Reason--selected { }
border-color: var(--docsearch-highlight-color);
background: var(--docsearch-soft-primary-color);
color: var(--docsearch-highlight-color);
font-weight: 500;
} }
.DocSearch-Feedback-Panel-Reason svg { .DocSearch-Feedback-Panel-Reason svg {
@ -365,8 +358,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 36px; padding-block: 0.5em;
padding: 6px 15px; padding-inline: 1em;
border: 0; border: 0;
border-radius: var(--docsearch-border-radius); border-radius: var(--docsearch-border-radius);
font-family: inherit; font-family: inherit;
@ -502,9 +495,9 @@
padding-inline: 1em; padding-inline: 1em;
padding-block: 1em; padding-block: 1em;
gap: 8px; gap: 8px;
background-color: var(--docsearch-background-color); background-color: var(--docsearch-error-soft-color);
border-radius: var(--docsearch-modal-radius); border-radius: var(--docsearch-border-radius);
color: var(--docsearch-text-color); color: var(--docsearch-error-text-color);
font-size: 1em; font-size: 1em;
font-weight: 400; font-weight: 400;
flex-direction: row; flex-direction: row;
@ -525,6 +518,7 @@
.DocSearch-AskAiScreen-Error p { .DocSearch-AskAiScreen-Error p {
margin: 0; margin: 0;
color: var(--docsearch-error-text-color);
} }
.DocSearch-AskAiScreen-Error-Content { .DocSearch-AskAiScreen-Error-Content {
@ -543,7 +537,7 @@
margin: 12px 0 8px; margin: 12px 0 8px;
color: var(--docsearch-text-color); color: var(--docsearch-text-color);
font-size: 12px; font-size: 12px;
border: 1px solid #febdc5; border: 1px solid var(--docsearch-error-soft-color);
animation: slide-down 0.3s ease-out; animation: slide-down 0.3s ease-out;
width: 100%; width: 100%;
} }
@ -679,7 +673,8 @@
} }
.DocSearch-Markdown-Content p { .DocSearch-Markdown-Content p {
margin: 1em 0; margin-block: 1em;
margin-inline: 0;
} }
.DocSearch-Markdown-Content p:last-child { .DocSearch-Markdown-Content p:last-child {
@ -691,11 +686,12 @@
} }
.DocSearch-Markdown-Content code { .DocSearch-Markdown-Content code {
background-color: var(--docsearch-key-background); background-color: var(--docsearch-code-block-background);
color: var(--docsearch-text-color); color: var(--docsearch-text-color);
padding: 0.2em 0; padding-block: 0.2em;
padding-inline: 0;
margin: 0; margin: 0;
border-radius: 3px; border-radius: var(--docsearch-border-radius);
font-family: font-family:
ui-monospace, ui-monospace,
SFMono-Regular, SFMono-Regular,
@ -704,17 +700,19 @@
Consolas, Consolas,
'Liberation Mono', 'Liberation Mono',
monospace; monospace;
font-size: 0.9em; font-size: 0.9rem;
letter-spacing: normal; letter-spacing: normal;
} }
.DocSearch-Markdown-Content pre { .DocSearch-Markdown-Content pre {
background-color: var(--docsearch-key-background); background-color: var(--docsearch-code-block-background);
color: var(--docsearch-text-color); color: var(--docsearch-text-color);
padding: 1.2em; padding-block: 1.2em;
border-radius: 6px; padding-inline: 1.2em;
border-radius: var(--docsearch-border-radius);
overflow-x: auto; overflow-x: auto;
margin: 1.5em 0; margin-block: 1.5em;
margin-inline: 0;
} }
.DocSearch-Markdown-Content pre code { .DocSearch-Markdown-Content pre code {
@ -722,7 +720,6 @@
color: inherit; color: inherit;
padding: 0; padding: 0;
margin: 0; margin: 0;
font-size: 0.8em;
border-radius: 0; border-radius: 0;
white-space: pre-wrap; white-space: pre-wrap;
overflow-wrap: break-word; overflow-wrap: break-word;
@ -1059,7 +1056,8 @@
.DocSearch-AskAiScreen-Sources { .DocSearch-AskAiScreen-Sources {
padding-inline: 0; padding-inline: 0;
margin-top: 0.5rem; margin-block: 0;
margin-block-start: 0.5rem;
color: var(--docsearch-text-color); color: var(--docsearch-text-color);
list-style-type: none; list-style-type: none;
display: flex; display: flex;
@ -1100,11 +1098,12 @@
position: absolute; position: absolute;
inset-block-start: 8px; inset-block-start: 8px;
inset-inline-end: 8px; inset-inline-end: 8px;
background: var(--docsearch-key-background); background: var(--docsearch-code-block-background);
color: var(--docsearch-text-color); color: var(--docsearch-text-color);
border: 0; border: 0;
padding: 0.2em 0.6em; padding-block: 0.2em;
font-size: 0.75em; padding-inline: 0.6em;
font-size: 0.95em;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;

View file

@ -7,6 +7,8 @@
--docsearch-subtle-color: rgb(214 214 231); --docsearch-subtle-color: rgb(214 214 231);
--docsearch-text-color: rgb(35 38 59 / 100%); --docsearch-text-color: rgb(35 38 59 / 100%);
--docsearch-error-color: #ef5350; --docsearch-error-color: #ef5350;
--docsearch-error-soft-color: rgb(254 189 197);
--docsearch-error-text-color: rgb(122 28 43);
--docsearch-success-color: #e8f5e9; --docsearch-success-color: #e8f5e9;
--docsearch-secondary-text-color: rgb(90 94 154 / 100%); --docsearch-secondary-text-color: rgb(90 94 154 / 100%);
--docsearch-background-color: rgb(245 245 250); --docsearch-background-color: rgb(245 245 250);
@ -88,6 +90,9 @@
0 0 0 1px rgb(33 36 61 / 5%), 0 0 0 1px rgb(33 36 61 / 5%),
0 4px 8px -2px rgb(33 36 61 / 25%); 0 4px 8px -2px rgb(33 36 61 / 25%);
--docsearch-popover-arrow-color: rgb(230 230 240); --docsearch-popover-arrow-color: rgb(230 230 240);
/* markdown */
--docsearch-code-block-background: rgb(235 235 240);
} }
/* Darkmode */ /* Darkmode */
@ -143,4 +148,7 @@
0 0 0 1px rgb(255 255 255 / 8%), 0 0 0 1px rgb(255 255 255 / 8%),
0 4px 8px -2px rgb(0 0 0 / 55%); 0 4px 8px -2px rgb(0 0 0 / 55%);
--docsearch-popover-arrow-color: var(--docsearch-hit-focus-background); --docsearch-popover-arrow-color: var(--docsearch-hit-focus-background);
/* markdown */
--docsearch-code-block-background: rgb(33 33 57);
} }

View file

@ -717,6 +717,14 @@
margin: 0.5em 0; margin: 0.5em 0;
} }
.DocSearch-Sidepanel .DocSearch-Markdown-Content code {
font-size: 0.8rem;
}
.DocSearch-Sidepanel .DocSearch-CodeSnippet pre {
padding-block-start: 2.4em;
}
.DocSearch-Sidepanel .DocSearch-AskAiScreen-MessageContent-Tool-Query:hover { .DocSearch-Sidepanel .DocSearch-AskAiScreen-MessageContent-Tool-Query:hover {
cursor: unset; cursor: unset;
box-shadow: none; box-shadow: none;

View file

@ -241,9 +241,7 @@ function AskAiExchangeCard({
<div className="DocSearch-AskAiScreen-Response-Container"> <div className="DocSearch-AskAiScreen-Response-Container">
<div className="DocSearch-AskAiScreen-Response"> <div className="DocSearch-AskAiScreen-Response">
<div className="DocSearch-AskAiScreen-Message DocSearch-AskAiScreen-Message--user"> <div className="DocSearch-AskAiScreen-Message DocSearch-AskAiScreen-Message--user">
<p className="DocSearch-AskAiScreen-Query"> <p className="DocSearch-AskAiScreen-Query">{userContent ?? ''}</p>
{userContent?.text ?? ''}
</p>
</div> </div>
<div className="DocSearch-AskAiScreen-Message DocSearch-AskAiScreen-Message--assistant"> <div className="DocSearch-AskAiScreen-Message DocSearch-AskAiScreen-Message--assistant">
<div className="DocSearch-AskAiScreen-MessageContent"> <div className="DocSearch-AskAiScreen-MessageContent">
@ -361,7 +359,7 @@ function AskAiExchangeCard({
<FeedbackActions <FeedbackActions
id={messageId} id={messageId}
showActions={showActions} showActions={showActions}
latestAssistantMessageContent={assistantContent?.text || null} latestAssistantMessageContent={assistantContent || null}
translations={translations} translations={translations}
conversations={conversations} conversations={conversations}
onFeedback={onFeedback} onFeedback={onFeedback}

View file

@ -63,7 +63,7 @@ export type ConversationScreenTranslations = Partial<
feedbackTagSafetyOrLegal: string; feedbackTagSafetyOrLegal: string;
feedbackTagOther: string; feedbackTagOther: string;
/** Error title shown if there is an error while chatting. */ /** Error title shown if there is an error while chatting. */
errorTitleText; errorTitleText: string;
suggestedPromptsTitleText: string; suggestedPromptsTitleText: string;
} }
>; >;
@ -171,9 +171,7 @@ const ConversationExchange = React.forwardRef<
> >
<div className="DocSearch-AskAiScreen-Response"> <div className="DocSearch-AskAiScreen-Response">
<div className="DocSearch-AskAiScreen-Message DocSearch-AskAiScreen-Message--user"> <div className="DocSearch-AskAiScreen-Message DocSearch-AskAiScreen-Message--user">
<p className="DocSearch-AskAiScreen-Query"> <p className="DocSearch-AskAiScreen-Query">{userContent ?? ''}</p>
{userContent?.text ?? ''}
</p>
</div> </div>
<div className="DocSearch-AskAiScreen-Message DocSearch-AskAiScreen-Message--assistant"> <div className="DocSearch-AskAiScreen-Message DocSearch-AskAiScreen-Message--assistant">
<div className="DocSearch-AskAiScreen-MessageContent"> <div className="DocSearch-AskAiScreen-MessageContent">
@ -288,7 +286,7 @@ const ConversationExchange = React.forwardRef<
isSidepanel={true} isSidepanel={true}
id={messageId} id={messageId}
showActions={showActions} showActions={showActions}
latestAssistantMessageContent={assistantContent?.text || null} latestAssistantMessageContent={assistantContent || null}
translations={translations} translations={translations}
conversations={conversations} conversations={conversations}
onFeedback={onFeedback} onFeedback={onFeedback}

View file

@ -1,7 +1,7 @@
import { render } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
import type { UIMessage } from 'ai'; import type { UIMessage } from 'ai';
import React from 'react'; import React from 'react';
import { describe, it, expect } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import '@testing-library/jest-dom/vitest'; import '@testing-library/jest-dom/vitest';
import { AskAiScreen } from '../AskAiScreen'; import { AskAiScreen } from '../AskAiScreen';
@ -17,6 +17,7 @@ const baseProps = {
setQuery: (): void => {}, setQuery: (): void => {},
messages: [], messages: [],
status: 'ready' as const, status: 'ready' as const,
tools: {},
disableUserPersonalization: false, disableUserPersonalization: false,
resultsFooterComponent: null, resultsFooterComponent: null,
} as any; } as any;
@ -47,4 +48,46 @@ describe('AskAiScreen', () => {
expect(getByText('oh no')).toBeInTheDocument(); expect(getByText('oh no')).toBeInTheDocument();
}); });
it('copies all assistant text parts around tool calls', () => {
const writeText = vi.fn();
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText },
});
const messages: UIMessage[] = [
{
id: 'user-1',
role: 'user',
parts: [{ type: 'text', text: 'What is Docusaurus?' }],
},
{
id: 'assistant-1',
role: 'assistant',
parts: [
{ type: 'text', text: 'Let me look that up.', state: 'done' },
{
type: 'tool-algolia_search_index',
toolCallId: 'tool-1',
state: 'output-available',
input: { query: 'Docusaurus', index: 'docs' },
output: { hits: [] },
},
{
type: 'text',
text: 'Docusaurus is a static site generator.',
state: 'done',
},
],
},
];
render(<AskAiScreen {...baseProps} messages={messages} />);
fireEvent.click(screen.getByTitle('Copy'));
expect(writeText).toHaveBeenCalledWith(
'Let me look that up.\n\nDocusaurus is a static site generator.'
);
});
}); });

View file

@ -214,7 +214,7 @@ export function FeedbackActions({
); );
return ( return (
<div className="DocSearch-AskAiScreen-Actions"> <>
<div className="DocSearch-AskAiScreen-Actions-Controls"> <div className="DocSearch-AskAiScreen-Actions-Controls">
{isSidepanel ? copyButton : null} {isSidepanel ? copyButton : null}
@ -259,7 +259,7 @@ export function FeedbackActions({
onClose={handleCloseNote} onClose={handleCloseNote}
/> />
) : null} ) : null}
</div> </>
); );
} }
@ -286,6 +286,7 @@ function NegativeFeedbackPanel({
onSubmit, onSubmit,
onClose, onClose,
}: NegativeFeedbackPanelProps): JSX.Element { }: NegativeFeedbackPanelProps): JSX.Element {
const feedbackTitleId = React.useId();
const { const {
feedbackPanelTitle = 'What went wrong? (optional)', feedbackPanelTitle = 'What went wrong? (optional)',
feedbackDetailsPlaceholder = 'Share some details...', feedbackDetailsPlaceholder = 'Share some details...',
@ -297,10 +298,12 @@ function NegativeFeedbackPanel({
<div <div
className="DocSearch-Feedback-Panel" className="DocSearch-Feedback-Panel"
role="group" role="group"
aria-label={feedbackPanelTitle} aria-labelledby={feedbackTitleId}
> >
<div className="DocSearch-Feedback-Panel-Header"> <div className="DocSearch-Feedback-Panel-Header">
<p className="DocSearch-Feedback-Panel-Title">{feedbackPanelTitle}</p> <p className="DocSearch-Feedback-Panel-Title" id={feedbackTitleId}>
{feedbackPanelTitle}
</p>
</div> </div>
<div className="DocSearch-Feedback-Panel-Reasons"> <div className="DocSearch-Feedback-Panel-Reasons">
@ -310,9 +313,7 @@ function NegativeFeedbackPanel({
<button <button
key={reason.value} key={reason.value}
type="button" type="button"
className={`DocSearch-Feedback-Panel-Reason${ className="DocSearch-Feedback-Panel-Reason"
isSelected ? ' DocSearch-Feedback-Panel-Reason--selected' : ''
}`}
aria-pressed={isSelected} aria-pressed={isSelected}
onClick={() => onToggleTag(reason.value)} onClick={() => onToggleTag(reason.value)}
> >

View file

@ -156,9 +156,20 @@ describe('FeedbackActions', () => {
const { onFeedback } = renderComponent(); const { onFeedback } = renderComponent();
fireEvent.click(screen.getByTitle('Dislike')); fireEvent.click(screen.getByTitle('Dislike'));
const tag = screen.getByText('Incorrect or incomplete'); const tag = screen.getByRole('button', {
name: 'Incorrect or incomplete',
});
expect(tag).toHaveAttribute('aria-pressed', 'false');
fireEvent.click(tag); fireEvent.click(tag);
expect(tag).toHaveAttribute('aria-pressed', 'true');
fireEvent.click(tag); fireEvent.click(tag);
expect(tag).toHaveAttribute('aria-pressed', 'false');
fireEvent.click(screen.getByText('Submit')); fireEvent.click(screen.getByText('Submit'));
await waitFor(() => await waitFor(() =>

View file

@ -1,5 +1,6 @@
import { Popover as PopoverPrimitive } from '@base-ui/react/popover'; import { Popover as PopoverPrimitive } from '@base-ui/react/popover';
import React from 'react'; import React from 'react';
import type { JSX } from 'react';
import { CloseIcon } from '../../icons'; import { CloseIcon } from '../../icons';
@ -7,13 +8,13 @@ export function Popover({ ...props }: PopoverPrimitive.Root.Props) {
return <PopoverPrimitive.Root {...props} />; return <PopoverPrimitive.Root {...props} />;
} }
function PopoverTrigger({ const PopoverTrigger = React.forwardRef<
className, HTMLButtonElement,
...props PopoverPrimitive.Trigger.Props
}: PopoverPrimitive.Trigger.Props) { >(({ className, ...props }, ref): JSX.Element => {
const cn = `DocSearch-Popover-Trigger${className ? ` ${className}` : ''}`; const cn = `DocSearch-Popover-Trigger${className ? ` ${className}` : ''}`;
return <PopoverPrimitive.Trigger className={cn} {...props} />; return <PopoverPrimitive.Trigger className={cn} ref={ref} {...props} />;
} });
Popover.Trigger = PopoverTrigger; Popover.Trigger = PopoverTrigger;

View file

@ -129,7 +129,6 @@ export const useAskAi: UseAskAi = ({
assistantId, assistantId,
apiKey, apiKey,
appId, appId,
indexName,
tools = EMPTY_TOOLS, tools = EMPTY_TOOLS,
searchParameters, searchParameters,
memory, memory,
@ -218,7 +217,7 @@ export const useAskAi: UseAskAi = ({
const conversations = useRef( const conversations = useRef(
createStoredConversations<StoredAskAiState>({ createStoredConversations<StoredAskAiState>({
key: `__DOCSEARCH_ASKAI_CONVERSATIONS__${indexName}`, key: `__DOCSEARCH_ASKAI_CONVERSATIONS__${appId}`,
limit: 10, limit: 10,
}) })
).current; ).current;

View file

@ -11,6 +11,7 @@ import {
isAIToolPart, isAIToolPart,
isAlgoliaMCPSearchOutputPart, isAlgoliaMCPSearchOutputPart,
sanitizeMessagesForRequest, sanitizeMessagesForRequest,
getMessageContent,
} from '../ai'; } from '../ai';
function message(id: string, parts: AIMessagePart[]): AIMessage { function message(id: string, parts: AIMessagePart[]): AIMessage {
@ -247,3 +248,50 @@ describe('getSearchToolQueries', () => {
expect(getSearchToolQueries(part)).toEqual(['foo']); expect(getSearchToolQueries(part)).toEqual(['foo']);
}); });
}); });
describe('getMessageContent', () => {
it('joins the text parts around tool calls so copying returns the full answer', () => {
expect(
getMessageContent(
message('a1', [
{ type: 'text', text: 'Let me look that up.', state: 'done' },
{
type: 'tool-algolia_search_index',
toolCallId: 't1',
state: 'output-available',
input: {
query: 'foo',
index: 'bar',
},
output: { hits: [] },
},
{
type: 'text',
text: 'Docusaurus is a static site generator.',
state: 'done',
},
])
)
).toBe('Let me look that up.\n\nDocusaurus is a static site generator.');
});
it('returns an empty string without a message or text parts', () => {
expect(getMessageContent(null)).toBe('');
expect(
getMessageContent(
message('a2', [
{
type: 'tool-algolia_search_index',
toolCallId: 't2',
state: 'output-available',
input: {
query: 'foo',
index: 'bar',
},
output: { hits: [] },
},
])
)
).toBe('');
});
});

View file

@ -113,10 +113,14 @@ export const buildDummyAskAiHit = (
}; };
}; };
export const getMessageContent = ( // answers can interleave text with tool calls, so join every text part
message: AIMessage | null // instead of stopping at the first one
): TextUIPart | undefined => // see https://github.com/algolia/docsearch/issues/2782
message?.parts.find((part) => part.type === 'text'); export const getMessageContent = (message: AIMessage | null): string =>
(message?.parts ?? [])
.filter((part): part is TextUIPart => part.type === 'text')
.map((part) => part.text)
.join('\n\n');
/** Helper function to check if error is a thread depth error (AI-217). */ /** Helper function to check if error is a thread depth error (AI-217). */
export function isThreadDepthError(error?: Error): boolean { export function isThreadDepthError(error?: Error): boolean {