diff --git a/.changeset/ready-clubs-serve.md b/.changeset/ready-clubs-serve.md
new file mode 100644
index 00000000..34c8173e
--- /dev/null
+++ b/.changeset/ready-clubs-serve.md
@@ -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
+```
diff --git a/.oxlintrc.json b/.oxlintrc.json
index 263b1f40..b0e55f20 100644
--- a/.oxlintrc.json
+++ b/.oxlintrc.json
@@ -317,7 +317,8 @@
"typescript/prefer-namespace-keyword": ["error"],
"typescript/prefer-ts-expect-error": ["error"],
"typescript/unified-signatures": ["error"],
- "vitest/require-mock-type-parameters": "off"
+ "vitest/require-mock-type-parameters": "off",
+ "no-debugger": ["error"]
},
"overrides": [
{
diff --git a/AGENTS.md b/AGENTS.md
index eeb033d0..bbad5ad1 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -78,27 +78,6 @@ bun run pw:run:webkit
## 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
- Use `type` imports for type-only imports: `import type { Foo } from './types'`
diff --git a/examples/demo-react/src/App.tsx b/examples/demo-react/src/App.tsx
index 2f554e44..fc57713b 100644
--- a/examples/demo-react/src/App.tsx
+++ b/examples/demo-react/src/App.tsx
@@ -1,6 +1,5 @@
/* eslint-disable react/react-in-jsx-scope */
import { version } from '@docsearch/react';
-import { DocSearchSidepanel } from '@docsearch/react/sidepanel';
import { useState, type JSX } from 'react';
import './App.css';
@@ -121,17 +120,6 @@ function App(): JSX.Element {
-
-
);
}
diff --git a/packages/docsearch-css/src/_askai.css b/packages/docsearch-css/src/_askai.css
index 1404cf3d..69d1e1ce 100644
--- a/packages/docsearch-css/src/_askai.css
+++ b/packages/docsearch-css/src/_askai.css
@@ -201,19 +201,12 @@
.DocSearch-AskAiScreen-Answer-Footer {
display: flex;
- flex-direction: row;
+ flex-flow: row wrap;
align-items: center;
justify-content: space-between;
gap: 8px;
}
-.DocSearch-AskAiScreen-Actions {
- display: flex;
- flex-direction: column;
- width: 100%;
- gap: var(--docsearch-spacing);
-}
-
.DocSearch-AskAiScreen-Actions-Controls {
display: flex;
flex-direction: row;
@@ -226,13 +219,16 @@
.DocSearch-Feedback-Panel {
flex-basis: 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);
display: flex;
flex-direction: column;
- gap: 12px;
+ gap: 0.75rem;
text-align: start;
animation: fade-in 0.2s ease-in forwards;
+ font-size: 0.875rem;
}
.DocSearch-Feedback-Panel-Header {
@@ -253,7 +249,8 @@
display: flex;
align-items: center;
justify-content: center;
- padding: 4px;
+ padding-inline: 0.25rem;
+ padding-block: 0.25rem;
border: 0;
background: none;
cursor: pointer;
@@ -267,8 +264,8 @@
}
.DocSearch-Feedback-Panel-Close svg {
- width: 16px;
- height: 16px;
+ width: 1rem;
+ height: 1rem;
}
.DocSearch-Feedback-Panel-Reasons {
@@ -280,8 +277,9 @@
.DocSearch-Feedback-Panel-Reason {
display: inline-flex;
align-items: center;
- gap: 6px;
- padding: 6px 12px;
+ gap: 0.375rem;
+ padding-block: 0.375rem;
+ padding-inline: 0.75rem;
border-radius: var(--docsearch-border-radius);
border: 1px solid var(--docsearch-subtle-color);
background: var(--docsearch-hit-background);
@@ -293,18 +291,13 @@
border-color 0.2s ease,
background-color 0.2s ease,
color 0.2s ease;
-}
-.DocSearch-Feedback-Panel-Reason:hover {
- border-color: var(--docsearch-highlight-color);
- background: var(--docsearch-hit-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;
+ &:hover,
+ &[aria-pressed='true'] {
+ border-color: var(--docsearch-highlight-color);
+ background: var(--docsearch-hit-highlight-color);
+ color: var(--docsearch-highlight-color);
+ }
}
.DocSearch-Feedback-Panel-Reason svg {
@@ -365,8 +358,8 @@
display: flex;
align-items: center;
justify-content: center;
- min-height: 36px;
- padding: 6px 15px;
+ padding-block: 0.5em;
+ padding-inline: 1em;
border: 0;
border-radius: var(--docsearch-border-radius);
font-family: inherit;
@@ -502,9 +495,9 @@
padding-inline: 1em;
padding-block: 1em;
gap: 8px;
- background-color: var(--docsearch-background-color);
- border-radius: var(--docsearch-modal-radius);
- color: var(--docsearch-text-color);
+ background-color: var(--docsearch-error-soft-color);
+ border-radius: var(--docsearch-border-radius);
+ color: var(--docsearch-error-text-color);
font-size: 1em;
font-weight: 400;
flex-direction: row;
@@ -525,6 +518,7 @@
.DocSearch-AskAiScreen-Error p {
margin: 0;
+ color: var(--docsearch-error-text-color);
}
.DocSearch-AskAiScreen-Error-Content {
@@ -543,7 +537,7 @@
margin: 12px 0 8px;
color: var(--docsearch-text-color);
font-size: 12px;
- border: 1px solid #febdc5;
+ border: 1px solid var(--docsearch-error-soft-color);
animation: slide-down 0.3s ease-out;
width: 100%;
}
@@ -679,7 +673,8 @@
}
.DocSearch-Markdown-Content p {
- margin: 1em 0;
+ margin-block: 1em;
+ margin-inline: 0;
}
.DocSearch-Markdown-Content p:last-child {
@@ -691,11 +686,12 @@
}
.DocSearch-Markdown-Content code {
- background-color: var(--docsearch-key-background);
+ background-color: var(--docsearch-code-block-background);
color: var(--docsearch-text-color);
- padding: 0.2em 0;
+ padding-block: 0.2em;
+ padding-inline: 0;
margin: 0;
- border-radius: 3px;
+ border-radius: var(--docsearch-border-radius);
font-family:
ui-monospace,
SFMono-Regular,
@@ -704,17 +700,19 @@
Consolas,
'Liberation Mono',
monospace;
- font-size: 0.9em;
+ font-size: 0.9rem;
letter-spacing: normal;
}
.DocSearch-Markdown-Content pre {
- background-color: var(--docsearch-key-background);
+ background-color: var(--docsearch-code-block-background);
color: var(--docsearch-text-color);
- padding: 1.2em;
- border-radius: 6px;
+ padding-block: 1.2em;
+ padding-inline: 1.2em;
+ border-radius: var(--docsearch-border-radius);
overflow-x: auto;
- margin: 1.5em 0;
+ margin-block: 1.5em;
+ margin-inline: 0;
}
.DocSearch-Markdown-Content pre code {
@@ -722,7 +720,6 @@
color: inherit;
padding: 0;
margin: 0;
- font-size: 0.8em;
border-radius: 0;
white-space: pre-wrap;
overflow-wrap: break-word;
@@ -1059,7 +1056,8 @@
.DocSearch-AskAiScreen-Sources {
padding-inline: 0;
- margin-top: 0.5rem;
+ margin-block: 0;
+ margin-block-start: 0.5rem;
color: var(--docsearch-text-color);
list-style-type: none;
display: flex;
@@ -1100,11 +1098,12 @@
position: absolute;
inset-block-start: 8px;
inset-inline-end: 8px;
- background: var(--docsearch-key-background);
+ background: var(--docsearch-code-block-background);
color: var(--docsearch-text-color);
border: 0;
- padding: 0.2em 0.6em;
- font-size: 0.75em;
+ padding-block: 0.2em;
+ padding-inline: 0.6em;
+ font-size: 0.95em;
border-radius: 4px;
cursor: pointer;
transition: opacity 0.2s ease;
diff --git a/packages/docsearch-css/src/_variables.css b/packages/docsearch-css/src/_variables.css
index 75e37d3b..8fe577a1 100644
--- a/packages/docsearch-css/src/_variables.css
+++ b/packages/docsearch-css/src/_variables.css
@@ -7,6 +7,8 @@
--docsearch-subtle-color: rgb(214 214 231);
--docsearch-text-color: rgb(35 38 59 / 100%);
--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-secondary-text-color: rgb(90 94 154 / 100%);
--docsearch-background-color: rgb(245 245 250);
@@ -88,6 +90,9 @@
0 0 0 1px rgb(33 36 61 / 5%),
0 4px 8px -2px rgb(33 36 61 / 25%);
--docsearch-popover-arrow-color: rgb(230 230 240);
+
+ /* markdown */
+ --docsearch-code-block-background: rgb(235 235 240);
}
/* Darkmode */
@@ -143,4 +148,7 @@
0 0 0 1px rgb(255 255 255 / 8%),
0 4px 8px -2px rgb(0 0 0 / 55%);
--docsearch-popover-arrow-color: var(--docsearch-hit-focus-background);
+
+ /* markdown */
+ --docsearch-code-block-background: rgb(33 33 57);
}
diff --git a/packages/docsearch-css/src/sidepanel.css b/packages/docsearch-css/src/sidepanel.css
index f680280b..5a9f56b5 100644
--- a/packages/docsearch-css/src/sidepanel.css
+++ b/packages/docsearch-css/src/sidepanel.css
@@ -717,6 +717,14 @@
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 {
cursor: unset;
box-shadow: none;
diff --git a/packages/docsearch-react/src/AskAiScreen.tsx b/packages/docsearch-react/src/AskAiScreen.tsx
index d0f72c1b..219fafa9 100644
--- a/packages/docsearch-react/src/AskAiScreen.tsx
+++ b/packages/docsearch-react/src/AskAiScreen.tsx
@@ -241,9 +241,7 @@ function AskAiExchangeCard({
-
- {userContent?.text ?? ''}
-
+
{userContent ?? ''}
@@ -361,7 +359,7 @@ function AskAiExchangeCard({
;
@@ -171,9 +171,7 @@ const ConversationExchange = React.forwardRef<
>
-
- {userContent?.text ?? ''}
-
+
{userContent ?? ''}
@@ -288,7 +286,7 @@ const ConversationExchange = React.forwardRef<
isSidepanel={true}
id={messageId}
showActions={showActions}
- latestAssistantMessageContent={assistantContent?.text || null}
+ latestAssistantMessageContent={assistantContent || null}
translations={translations}
conversations={conversations}
onFeedback={onFeedback}
diff --git a/packages/docsearch-react/src/__tests__/askai.test.tsx b/packages/docsearch-react/src/__tests__/askai.test.tsx
index b25e1b93..36698126 100644
--- a/packages/docsearch-react/src/__tests__/askai.test.tsx
+++ b/packages/docsearch-react/src/__tests__/askai.test.tsx
@@ -1,7 +1,7 @@
-import { render } from '@testing-library/react';
+import { fireEvent, render, screen } from '@testing-library/react';
import type { UIMessage } from 'ai';
import React from 'react';
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, vi } from 'vitest';
import '@testing-library/jest-dom/vitest';
import { AskAiScreen } from '../AskAiScreen';
@@ -17,6 +17,7 @@ const baseProps = {
setQuery: (): void => {},
messages: [],
status: 'ready' as const,
+ tools: {},
disableUserPersonalization: false,
resultsFooterComponent: null,
} as any;
@@ -47,4 +48,46 @@ describe('AskAiScreen', () => {
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(
);
+
+ fireEvent.click(screen.getByTitle('Copy'));
+
+ expect(writeText).toHaveBeenCalledWith(
+ 'Let me look that up.\n\nDocusaurus is a static site generator.'
+ );
+ });
});
diff --git a/packages/docsearch-react/src/components/FeedbackActions.tsx b/packages/docsearch-react/src/components/FeedbackActions.tsx
index b96c8f0a..4d16e3b7 100644
--- a/packages/docsearch-react/src/components/FeedbackActions.tsx
+++ b/packages/docsearch-react/src/components/FeedbackActions.tsx
@@ -214,7 +214,7 @@ export function FeedbackActions({
);
return (
-
+ <>
{isSidepanel ? copyButton : null}
@@ -259,7 +259,7 @@ export function FeedbackActions({
onClose={handleCloseNote}
/>
) : null}
-
+ >
);
}
@@ -286,6 +286,7 @@ function NegativeFeedbackPanel({
onSubmit,
onClose,
}: NegativeFeedbackPanelProps): JSX.Element {
+ const feedbackTitleId = React.useId();
const {
feedbackPanelTitle = 'What went wrong? (optional)',
feedbackDetailsPlaceholder = 'Share some details...',
@@ -297,10 +298,12 @@ function NegativeFeedbackPanel({
-
{feedbackPanelTitle}
+
+ {feedbackPanelTitle}
+
@@ -310,9 +313,7 @@ function NegativeFeedbackPanel({