Skip to content

Commit df20e6c

Browse files
authored
feat(askai): ai-sdk v5 (#2746)
* feat(askai): Initial ai-sdk v5 work * Add sdk version header, code cleanup * feat(askai): Get ai sdk v5 working * fix: tests bundlesize * fix: not all streaming states were showing * fix: Show reasoning state, fix CSS issues, extract link sources for multiple text parts * Remove important for removing text decoration
1 parent 5be3e84 commit df20e6c

File tree

15 files changed

+477
-244
lines changed

15 files changed

+477
-244
lines changed

bundlesize.config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
},
77
{
88
"path": "packages/docsearch-react/dist/umd/index.js",
9-
"maxSize": "75 kB"
9+
"maxSize": "108 kB"
1010
},
1111
{
1212
"path": "packages/docsearch-js/dist/umd/index.js",
13-
"maxSize": "90 kB"
13+
"maxSize": "121 kB"
1414
}
1515
]
1616
}

packages/docsearch-css/src/modal.css

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,6 @@ assistive tech users */
782782
display: flex;
783783
flex: 1 1 auto;
784784
font-weight: 400;
785-
line-height: 1.2em;
786785
overflow-x: hidden;
787786
position: relative;
788787
text-overflow: ellipsis;
@@ -800,6 +799,10 @@ assistive tech users */
800799
text-overflow: ellipsis;
801800
}
802801

802+
.DocSearch-Hit-AskAIButton-title mark {
803+
text-decoration: none;
804+
}
805+
803806
@keyframes fade-in {
804807
0% {
805808
opacity: 0;
@@ -938,7 +941,14 @@ assistive tech users */
938941
font-weight: 400;
939942
}
940943

941-
.DocSearch-AskAiScreen-Error svg {
944+
.DocSearch-AskAiScreen-MessageContent {
945+
display: flex;
946+
flex-direction: column;
947+
row-gap: 1em;
948+
}
949+
950+
.DocSearch-AskAiScreen-Error svg,
951+
.DocSearch-AskAiScreen-MessageContent-Tool svg {
942952
width: 16px;
943953
height: 16px;
944954
flex-shrink: 0;
@@ -1219,10 +1229,13 @@ assistive tech users */
12191229
color: var(--docsearch-muted-color);
12201230
}
12211231

1232+
.DocSearch-AskAiScreen-MessageContent-Reasoning svg {
1233+
color: var(--docsearch-icon-color);
1234+
}
1235+
12221236
.DocSearch-AskAiScreen-MessageContent-Tool {
12231237
display: flex;
1224-
padding: 1em 0;
1225-
align-items: center;
1238+
align-items: baseline;
12261239
width: 100%;
12271240
color: var(--docsearch-muted-color);
12281241
}

packages/docsearch-react/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@
3636
"watch": "nodemon --watch src --ext ts,tsx,js,jsx,json --ignore dist/ --ignore node_modules/ --verbose --delay 250ms --exec \"yarn on:change\""
3737
},
3838
"dependencies": {
39-
"@ai-sdk/react": "^1.2.12",
39+
"@ai-sdk/react": "^2.0.30",
4040
"@algolia/autocomplete-core": "1.19.2",
4141
"@docsearch/css": "4.0.0-beta.8",
42+
"ai": "^5.0.30",
4243
"algoliasearch": "^5.28.0",
4344
"marked": "^15.0.12"
4445
},

packages/docsearch-react/src/AskAiScreen.tsx

Lines changed: 108 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import type { UseChatHelpers } from '@ai-sdk/react';
2-
import React, { type JSX, useState, useEffect, useMemo } from 'react';
2+
import React, { type JSX, useMemo, useState, useEffect } from 'react';
33

44
import { AggregatedSearchBlock } from './AggregatedSearchBlock';
55
import { AlertIcon, LoadingIcon, SearchIcon } from './icons';
66
import { MemoizedMarkdown } from './MemoizedMarkdown';
77
import type { ScreenStateProps } from './ScreenState';
88
import type { StoredSearchPlugin } from './stored-searches';
99
import type { InternalDocSearchHit, StoredAskAiState } from './types';
10-
import { extractLinksFromText } from './utils/ai';
10+
import type { AIMessage } from './types/AskiAi';
11+
import { extractLinksFromMessage, getMessageContent } from './utils/ai';
1112
import { groupConsecutiveToolResults } from './utils/groupConsecutiveToolResults';
1213

1314
export type AskAiScreenTranslations = Partial<{
@@ -46,8 +47,8 @@ export type AskAiScreenTranslations = Partial<{
4647
}>;
4748

4849
type AskAiScreenProps = Omit<ScreenStateProps<InternalDocSearchHit>, 'translations'> & {
49-
messages: UseChatHelpers['messages'];
50-
status: UseChatHelpers['status'];
50+
messages: AIMessage[];
51+
status: UseChatHelpers<AIMessage>['status'];
5152
askAiStreamError: Error | null;
5253
askAiFetchError: Error | undefined;
5354
translations?: AskAiScreenTranslations;
@@ -59,8 +60,8 @@ interface AskAiScreenHeaderProps {
5960

6061
interface Exchange {
6162
id: string;
62-
userMessage: UseChatHelpers['messages'][number];
63-
assistantMessage: UseChatHelpers['messages'][number] | null;
63+
userMessage: AIMessage;
64+
assistantMessage: AIMessage | null;
6465
}
6566

6667
function AskAiScreenHeader({ disclaimerText }: AskAiScreenHeaderProps): JSX.Element {
@@ -71,7 +72,7 @@ interface AskAiExchangeCardProps {
7172
exchange: Exchange;
7273
askAiStreamError: Error | null;
7374
isLastExchange: boolean;
74-
loadingStatus: UseChatHelpers['status'];
75+
loadingStatus: UseChatHelpers<AIMessage>['status'];
7576
onSearchQueryClick: (query: string) => void;
7677
translations: AskAiScreenTranslations;
7778
conversations: StoredSearchPlugin<StoredAskAiState>;
@@ -92,20 +93,25 @@ function AskAiExchangeCard({
9293

9394
const showActions = !isLastExchange || (isLastExchange && loadingStatus === 'ready' && Boolean(assistantMessage));
9495

95-
const urlsToDisplay = React.useMemo(() => extractLinksFromText(assistantMessage?.content || ''), [assistantMessage]);
96+
const assistantContent = useMemo(() => getMessageContent(assistantMessage), [assistantMessage]);
97+
const userContent = useMemo(() => getMessageContent(userMessage), [userMessage]);
98+
99+
const urlsToDisplay = React.useMemo(() => extractLinksFromMessage(assistantMessage), [assistantMessage]);
96100

97101
const displayParts = React.useMemo(() => {
98-
if (!Array.isArray(assistantMessage?.parts)) {
99-
return assistantMessage?.content ? [assistantMessage?.content] : [];
100-
}
101102
return groupConsecutiveToolResults(assistantMessage?.parts || []);
102103
}, [assistantMessage]);
103104

105+
const isThinking =
106+
['submitted', 'streaming'].includes(loadingStatus) &&
107+
isLastExchange &&
108+
!displayParts.some((part) => part.type !== 'step-start');
109+
104110
return (
105111
<div className="DocSearch-AskAiScreen-Response-Container">
106112
<div className="DocSearch-AskAiScreen-Response">
107113
<div className="DocSearch-AskAiScreen-Message DocSearch-AskAiScreen-Message--user">
108-
<p className="DocSearch-AskAiScreen-Query">{userMessage.content}</p>
114+
<p className="DocSearch-AskAiScreen-Query">{userContent?.text ?? ''}</p>
109115
</div>
110116
<div className="DocSearch-AskAiScreen-Message DocSearch-AskAiScreen-Message--assistant">
111117
<div className="DocSearch-AskAiScreen-MessageContent">
@@ -120,127 +126,113 @@ function AskAiExchangeCard({
120126
/>
121127
</div>
122128
)}
123-
{loadingStatus === 'submitted' && isLastExchange && (
129+
{isThinking && (
124130
<div className="DocSearch-AskAiScreen-MessageContent-Reasoning">
125131
<span className="shimmer">{translations.thinkingText || 'Thinking...'}</span>
126132
</div>
127133
)}
128-
{Array.isArray(displayParts)
129-
? displayParts.map((part, idx) => {
130-
const index = idx;
131-
132-
if (typeof part === 'string') {
133-
return (
134-
<MemoizedMarkdown
135-
key={index}
136-
content={part}
137-
copyButtonText={translations.copyButtonText || 'Copy'}
138-
copyButtonCopiedText={translations.copyButtonCopiedText || 'Copied!'}
139-
isStreaming={loadingStatus === 'streaming'}
140-
/>
141-
);
142-
}
143-
144-
// aggregated tool call rendering
145-
if (part && (part as any).type === 'aggregated-tool-call') {
134+
{displayParts.map((part, idx) => {
135+
const index = idx;
136+
137+
if (typeof part === 'string') {
138+
return (
139+
<MemoizedMarkdown
140+
key={index}
141+
content={part}
142+
copyButtonText={translations.copyButtonText || 'Copy'}
143+
copyButtonCopiedText={translations.copyButtonCopiedText || 'Copied!'}
144+
isStreaming={loadingStatus === 'streaming'}
145+
/>
146+
);
147+
}
148+
149+
if (part.type === 'aggregated-tool-call') {
150+
return (
151+
<AggregatedSearchBlock
152+
key={index}
153+
queries={part.queries}
154+
translations={translations}
155+
onSearchQueryClick={onSearchQueryClick}
156+
/>
157+
);
158+
}
159+
160+
if (part.type === 'reasoning' && part.state === 'streaming') {
161+
return (
162+
<div key={index} className="DocSearch-AskAiScreen-MessageContent-Reasoning shimmer">
163+
<LoadingIcon className="DocSearch-AskAiScreen-SmallerLoadingIcon" />
164+
<span className="shimmer">Reasoning...</span>
165+
</div>
166+
);
167+
}
168+
169+
if (part.type === 'text') {
170+
return (
171+
<MemoizedMarkdown
172+
key={index}
173+
content={part.text}
174+
copyButtonText={translations.copyButtonText || 'Copy'}
175+
copyButtonCopiedText={translations.copyButtonCopiedText || 'Copied!'}
176+
isStreaming={part.state === 'streaming'}
177+
/>
178+
);
179+
}
180+
if (part.type === 'tool-searchIndex') {
181+
switch (part.state) {
182+
case 'input-streaming':
146183
return (
147-
<AggregatedSearchBlock
148-
key={index}
149-
queries={(part as any).queries}
150-
translations={translations}
151-
onSearchQueryClick={onSearchQueryClick}
152-
/>
153-
);
154-
}
155-
156-
if (part.type === 'reasoning' && assistantMessage?.parts?.length === 1) {
157-
return (
158-
<div key={index} className="DocSearch-AskAiScreen-MessageContent-Reasoning shimmer">
159-
<span className="shimmer">Reasoning...</span>
184+
<div key={index} className="DocSearch-AskAiScreen-MessageContent-Tool Tool--PartialCall shimmer">
185+
<LoadingIcon className="DocSearch-AskAiScreen-SmallerLoadingIcon" />
186+
<span>{translations.preToolCallText || 'Searching...'}</span>
160187
</div>
161188
);
162-
}
163-
164-
if (part.type === 'text') {
189+
case 'input-available':
165190
return (
166-
<MemoizedMarkdown
167-
key={index}
168-
content={part.text}
169-
copyButtonText={translations.copyButtonText || 'Copy'}
170-
copyButtonCopiedText={translations.copyButtonCopiedText || 'Copied!'}
171-
isStreaming={loadingStatus === 'streaming'}
172-
/>
191+
<div key={index} className="DocSearch-AskAiScreen-MessageContent-Tool Tool--Call shimmer">
192+
<LoadingIcon className="DocSearch-AskAiScreen-SmallerLoadingIcon" />
193+
<span>
194+
{`${translations.duringToolCallText || 'Searching for '} "${part.input || ''}" ...`}
195+
</span>
196+
</div>
173197
);
174-
}
175-
if (part.type === 'tool-invocation') {
176-
const { toolInvocation } = part;
177-
if (toolInvocation.toolName === 'searchIndex') {
178-
switch (toolInvocation.state) {
179-
case 'partial-call':
180-
return (
181-
<div
182-
key={index}
183-
className="DocSearch-AskAiScreen-MessageContent-Tool Tool--PartialCall shimmer"
184-
>
185-
<LoadingIcon className="DocSearch-AskAiScreen-SmallerLoadingIcon" />
186-
<span>{translations.preToolCallText || 'Searching...'}</span>
187-
</div>
188-
);
189-
case 'call':
190-
return (
191-
<div key={index} className="DocSearch-AskAiScreen-MessageContent-Tool Tool--Call shimmer">
192-
<LoadingIcon className="DocSearch-AskAiScreen-SmallerLoadingIcon" />
193-
<span>
194-
{`${translations.duringToolCallText || 'Searching for '} "${toolInvocation.args?.query || ''}" ...`}
195-
</span>
196-
</div>
197-
);
198-
case 'result':
199-
return (
200-
<div key={index} className="DocSearch-AskAiScreen-MessageContent-Tool Tool--Result">
201-
<SearchIcon size={18} />
202-
<span>
203-
{`${translations.afterToolCallText || 'Searched for'}`}{' '}
204-
<span
205-
role="button"
206-
tabIndex={0}
207-
className="DocSearch-AskAiScreen-MessageContent-Tool-Query"
208-
onKeyDown={(e) => {
209-
if (e.key === 'Enter' || e.key === ' ') {
210-
e.preventDefault();
211-
onSearchQueryClick(toolInvocation.args?.query || '');
212-
}
213-
}}
214-
onClick={() => onSearchQueryClick(toolInvocation.args?.query || '')}
215-
>
216-
{' '}
217-
&quot;{toolInvocation.args?.query || ''}&quot;
218-
</span>
219-
</span>
220-
</div>
221-
);
222-
default:
223-
return null;
224-
}
225-
}
226-
// fallback for unknown tool, should never happen in theory. :shrug:
198+
case 'output-available':
227199
return (
228-
<span key={index} className="text-sm italic shimmer">
229-
{translations.thinkingText || 'Thinking...'}
230-
</span>
200+
<div key={index} className="DocSearch-AskAiScreen-MessageContent-Tool Tool--Result">
201+
<SearchIcon />
202+
<span>
203+
{`${translations.afterToolCallText || 'Searched for'}`}{' '}
204+
<span
205+
role="button"
206+
tabIndex={0}
207+
className="DocSearch-AskAiScreen-MessageContent-Tool-Query"
208+
onKeyDown={(e) => {
209+
if (e.key === 'Enter' || e.key === ' ') {
210+
e.preventDefault();
211+
onSearchQueryClick(part.output.query || '');
212+
}
213+
}}
214+
onClick={() => onSearchQueryClick(part.output.query || '')}
215+
>
216+
{' '}
217+
&quot;{part.output.query || ''}&quot;
218+
</span>
219+
</span>
220+
</div>
231221
);
232-
}
233-
// fallback for unknown part type
234-
return null;
235-
})
236-
: assistantMessage?.content}
222+
default:
223+
break;
224+
}
225+
}
226+
// fallback for unknown part type
227+
return null;
228+
})}
237229
</div>
238230
</div>
239231
<div className="DocSearch-AskAiScreen-Answer-Footer">
240232
<AskAiScreenFooterActions
241233
id={userMessage?.id || exchange.id}
242234
showActions={showActions}
243-
latestAssistantMessageContent={assistantMessage?.content || null}
235+
latestAssistantMessageContent={assistantContent?.text || null}
244236
translations={translations}
245237
conversations={conversations}
246238
onFeedback={onFeedback}

0 commit comments

Comments
 (0)