1
0
Fork 0

feat: agent studio feedback integration (#2868)

Cherry-picked from v5 branch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dylan Tientcheu 2026-03-26 12:32:15 +01:00
parent e20d30de7b
commit f83f5a911c
9 changed files with 171 additions and 117 deletions

View file

@ -7,7 +7,8 @@ import './App.css';
import '@docsearch/css/dist/style.css'; import '@docsearch/css/dist/style.css';
import '@docsearch/css/dist/sidepanel.css'; import '@docsearch/css/dist/sidepanel.css';
import { AgentStudioExample } from './examples/agent-studio'; import AgentStudio from './examples/agent-studio';
import AgentStudioSidepanel from './examples/agent-studio-sidepanel';
import Basic from './examples/basic'; import Basic from './examples/basic';
import BasicAskAI from './examples/basic-askai'; import BasicAskAI from './examples/basic-askai';
import Composable from './examples/composable'; import Composable from './examples/composable';
@ -102,7 +103,14 @@ function App(): JSX.Element {
<section className="demo-section"> <section className="demo-section">
<p className="section-description">Agent Studio</p> <p className="section-description">Agent Studio</p>
<div className="search-wrapper column"> <div className="search-wrapper column">
<AgentStudioExample /> <AgentStudio />
</div>
</section>
<section className="demo-section">
<p className="section-description">Agent Studio sidepanel</p>
<div className="search-wrapper column">
<AgentStudioSidepanel />
</div> </div>
</section> </section>
</main> </main>

View file

@ -0,0 +1,25 @@
/* eslint-disable react/react-in-jsx-scope */
import { DocSearch } from '@docsearch/core';
import { SidepanelButton } from '@docsearch/sidepanel/button';
import { Sidepanel } from '@docsearch/sidepanel/sidepanel';
import type { JSX } from 'react';
export default function AgentStudioSidepanel(): JSX.Element {
return (
<DocSearch>
<SidepanelButton
variant="inline"
translations={{
buttonText: 'Agent Studio',
}}
/>
<Sidepanel
indexName="docsearch-markdown"
appId="PMZUYBQDAK"
apiKey="a00716d83c64f6c61905c078b7d5ab66"
assistantId="ccdec697-e3fe-465b-a1c3-657e7bf18aef"
agentStudio={true}
/>
</DocSearch>
);
}

View file

@ -2,45 +2,25 @@
import { DocSearch } from '@docsearch/core'; import { DocSearch } from '@docsearch/core';
import { DocSearchButton } from '@docsearch/modal/button'; import { DocSearchButton } from '@docsearch/modal/button';
import { DocSearchModal } from '@docsearch/modal/modal'; import { DocSearchModal } from '@docsearch/modal/modal';
import { SidepanelButton } from '@docsearch/sidepanel/button';
import { Sidepanel } from '@docsearch/sidepanel/sidepanel';
import type { JSX } from 'react'; import type { JSX } from 'react';
export function AgentStudioExample(): JSX.Element { export default function AgentStudio(): JSX.Element {
return ( return (
<> <DocSearch>
<DocSearch> <DocSearchButton
<DocSearchButton translations={{
translations={{ buttonText: 'Ask AI with Agent Studio',
buttonText: 'Ask AI with Agent Studio', }}
}} />
/> <DocSearchModal
<DocSearchModal indexName="docsearch"
indexName="docsearch" appId="PMZUYBQDAK"
appId="PMZUYBQDAK" apiKey="a00716d83c64f6c61905c078b7d5ab66"
apiKey="a00716d83c64f6c61905c078b7d5ab66" askAi={{
askAi={{ assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef', agentStudio: true,
agentStudio: true, }}
}} />
/> </DocSearch>
</DocSearch>
<DocSearch>
<SidepanelButton
variant="inline"
translations={{
buttonText: 'Agent Studio',
}}
/>
<Sidepanel
indexName="docsearch-markdown"
appId="PMZUYBQDAK"
apiKey="a00716d83c64f6c61905c078b7d5ab66"
assistantId="ccdec697-e3fe-465b-a1c3-657e7bf18aef"
agentStudio={true}
/>
</DocSearch>
</>
); );
} }

View file

@ -140,6 +140,8 @@ function AskAiExchangeCard({
isLastExchange && isLastExchange &&
!displayParts.some((part) => part.type !== 'step-start'); !displayParts.some((part) => part.type !== 'step-start');
const messageId = agentStudio ? assistantMessage?.id || exchange.id : userMessage?.id || exchange.id;
return ( return (
<div className="DocSearch-AskAiScreen-Response-Container"> <div className="DocSearch-AskAiScreen-Response-Container">
<div className="DocSearch-AskAiScreen-Response"> <div className="DocSearch-AskAiScreen-Response">
@ -236,12 +238,11 @@ function AskAiExchangeCard({
</div> </div>
<div className="DocSearch-AskAiScreen-Answer-Footer"> <div className="DocSearch-AskAiScreen-Answer-Footer">
<AskAiScreenFooterActions <AskAiScreenFooterActions
id={userMessage?.id || exchange.id} id={messageId}
showActions={showActions} showActions={showActions}
latestAssistantMessageContent={assistantContent?.text || null} latestAssistantMessageContent={assistantContent?.text || null}
translations={translations} translations={translations}
conversations={conversations} conversations={conversations}
agentStudio={agentStudio}
onFeedback={onFeedback} onFeedback={onFeedback}
/> />
</div> </div>
@ -262,7 +263,6 @@ interface AskAiScreenFooterActionsProps {
translations: AskAiScreenTranslations; translations: AskAiScreenTranslations;
conversations: StoredSearchPlugin<StoredAskAiState>; conversations: StoredSearchPlugin<StoredAskAiState>;
onFeedback?: (messageId: string, thumbs: 0 | 1) => Promise<void>; onFeedback?: (messageId: string, thumbs: 0 | 1) => Promise<void>;
agentStudio?: boolean;
} }
export function AskAiScreenFooterActions({ export function AskAiScreenFooterActions({
@ -272,7 +272,6 @@ export function AskAiScreenFooterActions({
translations, translations,
conversations, conversations,
onFeedback, onFeedback,
agentStudio,
}: AskAiScreenFooterActionsProps): JSX.Element | null { }: AskAiScreenFooterActionsProps): JSX.Element | null {
// local state for feedback, initialised from stored conversations // local state for feedback, initialised from stored conversations
const initialFeedback = React.useMemo(() => { const initialFeedback = React.useMemo(() => {
@ -310,26 +309,25 @@ export function AskAiScreenFooterActions({
return ( return (
<div className="DocSearch-AskAiScreen-Actions"> <div className="DocSearch-AskAiScreen-Actions">
{!agentStudio && {feedback === null ? (
(feedback === null ? ( <>
<> {saving ? (
{saving ? ( <LoadingIcon className="DocSearch-AskAiScreen-SmallerLoadingIcon" />
<LoadingIcon className="DocSearch-AskAiScreen-SmallerLoadingIcon" /> ) : (
) : ( <>
<> <LikeButton title={likeButtonTitle} onClick={() => handleFeedback('like')} />
<LikeButton title={likeButtonTitle} onClick={() => handleFeedback('like')} /> <DislikeButton title={dislikeButtonTitle} onClick={() => handleFeedback('dislike')} />
<DislikeButton title={dislikeButtonTitle} onClick={() => handleFeedback('dislike')} /> </>
</> )}
)} {savingError && (
{savingError && ( <p className="DocSearch-AskAiScreen-FeedbackText">{savingError.message || 'An error occured'}</p>
<p className="DocSearch-AskAiScreen-FeedbackText">{savingError.message || 'An error occured'}</p> )}
)} </>
</> ) : (
) : ( <p className="DocSearch-AskAiScreen-FeedbackText DocSearch-AskAiScreen-FeedbackText--visible">
<p className="DocSearch-AskAiScreen-FeedbackText DocSearch-AskAiScreen-FeedbackText--visible"> {thanksForFeedbackText}
{thanksForFeedbackText} </p>
</p> )}
))}
<CopyButton <CopyButton
translations={translations} translations={translations}
onClick={() => navigator.clipboard.writeText(latestAssistantMessageContent)} onClick={() => navigator.clipboard.writeText(latestAssistantMessageContent)}
@ -373,7 +371,7 @@ export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps):
startNewConversationButtonText = 'Start a new conversation', startNewConversationButtonText = 'Start a new conversation',
} = translations; } = translations;
const { messages, askAiError, status } = props; const { messages, askAiError, status, agentStudio } = props;
// Check if there's a thread depth error // Check if there's a thread depth error
const hasThreadDepthError = useMemo(() => { const hasThreadDepthError = useMemo(() => {
@ -448,7 +446,7 @@ export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps):
loadingStatus={props.status} loadingStatus={props.status}
translations={translations} translations={translations}
conversations={props.conversations} conversations={props.conversations}
agentStudio={props.agentStudio} agentStudio={agentStudio}
onSearchQueryClick={handleSearchQueryClick} onSearchQueryClick={handleSearchQueryClick}
onFeedback={props.onFeedback} onFeedback={props.onFeedback}
/> />

View file

@ -20,13 +20,12 @@ import type { ScreenStateTranslations } from './ScreenState';
import { ScreenState } from './ScreenState'; import { ScreenState } from './ScreenState';
import type { SearchBoxTranslations } from './SearchBox'; import type { SearchBoxTranslations } from './SearchBox';
import { SearchBox } from './SearchBox'; import { SearchBox } from './SearchBox';
import { createStoredConversations, createStoredSearches } from './stored-searches'; import { createStoredSearches } from './stored-searches';
import type { import type {
DocSearchHit, DocSearchHit,
DocSearchState, DocSearchState,
InternalDocSearchHit, InternalDocSearchHit,
StoredAskAiMessage, StoredAskAiMessage,
StoredAskAiState,
StoredDocSearchHit, StoredDocSearchHit,
SuggestedQuestionHit, SuggestedQuestionHit,
} from './types'; } from './types';
@ -384,12 +383,6 @@ export function DocSearchModal({
const defaultIndexName = indexes[0].name; const defaultIndexName = indexes[0].name;
// storage // storage
const conversations = React.useRef(
createStoredConversations<StoredAskAiState>({
key: `__DOCSEARCH_ASKAI_CONVERSATIONS__${askAiConfig?.indexName || defaultIndexName}`,
limit: 10,
}),
).current;
const favoriteSearches = React.useRef( const favoriteSearches = React.useRef(
createStoredSearches<StoredDocSearchHit>({ createStoredSearches<StoredDocSearchHit>({
key: `__DOCSEARCH_FAVORITE_SEARCHES__${defaultIndexName}`, key: `__DOCSEARCH_FAVORITE_SEARCHES__${defaultIndexName}`,
@ -405,15 +398,16 @@ export function DocSearchModal({
const [stoppedStream, setStoppedStream] = React.useState(false); const [stoppedStream, setStoppedStream] = React.useState(false);
const { messages, status, setMessages, sendMessage, stopAskAiStreaming, askAiError, sendFeedback } = useAskAi({ const { messages, status, setMessages, sendMessage, stopAskAiStreaming, askAiError, sendFeedback, conversations } =
assistantId: askAiConfigurationId, useAskAi({
apiKey: askAiConfig?.apiKey || apiKey, assistantId: askAiConfigurationId,
appId: askAiConfig?.appId || appId, apiKey: askAiConfig?.apiKey || apiKey,
indexName: askAiConfig?.indexName || defaultIndexName, appId: askAiConfig?.appId || appId,
searchParameters: askAiSearchParameters, indexName: askAiConfig?.indexName || defaultIndexName,
useStagingEnv: askAiUseStagingEnv, searchParameters: askAiSearchParameters,
agentStudio, useStagingEnv: askAiUseStagingEnv,
}); agentStudio,
});
const prevStatus = React.useRef(status); const prevStatus = React.useRef(status);
React.useEffect(() => { React.useEffect(() => {

View file

@ -14,7 +14,6 @@ interface ConversationActionsProps {
conversations: StoredSearchPlugin<StoredAskAiState>; conversations: StoredSearchPlugin<StoredAskAiState>;
onFeedback?: (messageId: string, thumbs: 0 | 1) => Promise<void>; onFeedback?: (messageId: string, thumbs: 0 | 1) => Promise<void>;
isSidepanel?: boolean; isSidepanel?: boolean;
agentStudio?: boolean;
} }
export function ConversationActions({ export function ConversationActions({
@ -24,7 +23,6 @@ export function ConversationActions({
onFeedback, onFeedback,
latestAssistantMessageContent, latestAssistantMessageContent,
showActions, showActions,
agentStudio,
}: ConversationActionsProps): JSX.Element | null { }: ConversationActionsProps): JSX.Element | null {
const initialFeedback = React.useMemo(() => { const initialFeedback = React.useMemo(() => {
const message = conversations.getOne?.(id); const message = conversations.getOne?.(id);
@ -65,26 +63,25 @@ export function ConversationActions({
translations={translations} translations={translations}
onClick={() => navigator.clipboard.writeText(latestAssistantMessageContent)} onClick={() => navigator.clipboard.writeText(latestAssistantMessageContent)}
/> />
{!agentStudio && {feedback === null ? (
(feedback === null ? ( <>
<> {saving ? (
{saving ? ( <LoadingIcon className="DocSearch-AskAiScreen-SmallerLoadingIcon" />
<LoadingIcon className="DocSearch-AskAiScreen-SmallerLoadingIcon" /> ) : (
) : ( <>
<> <LikeButton title={likeButtonTitle} onClick={() => handleFeedback('like')} />
<LikeButton title={likeButtonTitle} onClick={() => handleFeedback('like')} /> <DislikeButton title={dislikeButtonTitle} onClick={() => handleFeedback('dislike')} />
<DislikeButton title={dislikeButtonTitle} onClick={() => handleFeedback('dislike')} /> </>
</> )}
)} {savingError && (
{savingError && ( <p className="DocSearch-AskAiScreen-FeedbackText">{savingError.message || 'An error occured'}</p>
<p className="DocSearch-AskAiScreen-FeedbackText">{savingError.message || 'An error occured'}</p> )}
)} </>
</> ) : (
) : ( <p className="DocSearch-AskAiScreen-FeedbackText DocSearch-AskAiScreen-FeedbackText--visible">
<p className="DocSearch-AskAiScreen-FeedbackText DocSearch-AskAiScreen-FeedbackText--visible"> {thanksForFeedbackText}
{thanksForFeedbackText} </p>
</p> )}
))}
</div> </div>
); );
} }

View file

@ -120,6 +120,8 @@ const ConversationExchange = React.forwardRef<HTMLDivElement, ConversationnExcha
const showActions = const showActions =
!wasStopped && (!isLastExchange || (isLastExchange && status === 'ready' && Boolean(assistantMessage))); !wasStopped && (!isLastExchange || (isLastExchange && status === 'ready' && Boolean(assistantMessage)));
const messageId = agentStudio ? assistantMessage?.id || exchange.id : userMessage?.id || exchange.id;
return ( return (
<div className="DocSearch-AskAiScreen-Response-Container" ref={conversationRef}> <div className="DocSearch-AskAiScreen-Response-Container" ref={conversationRef}>
<div className="DocSearch-AskAiScreen-Response"> <div className="DocSearch-AskAiScreen-Response">
@ -212,12 +214,11 @@ const ConversationExchange = React.forwardRef<HTMLDivElement, ConversationnExcha
<div className="DocSearch-AskAiScreen-Answer-Footer"> <div className="DocSearch-AskAiScreen-Answer-Footer">
<ConversationActions <ConversationActions
id={userMessage?.id || exchange.id} id={messageId}
showActions={showActions} showActions={showActions}
latestAssistantMessageContent={assistantContent?.text || null} latestAssistantMessageContent={assistantContent?.text || null}
translations={translations} translations={translations}
conversations={conversations} conversations={conversations}
agentStudio={agentStudio}
onFeedback={onFeedback} onFeedback={onFeedback}
/> />
</div> </div>

View file

@ -3,6 +3,8 @@ import { ASK_AI_API_URL, BETA_ASK_AI_API_URL } from './constants';
// ... existing imports ... // ... existing imports ...
const TOKEN_KEY = 'askai_token'; const TOKEN_KEY = 'askai_token';
export const agentStudioBaseUrl = (appId: string): string => `https://${appId}.algolia.net/agent-studio/1`;
type TokenPayload = { exp: number }; type TokenPayload = { exp: number };
const decode = (token: string): TokenPayload => { const decode = (token: string): TokenPayload => {
@ -134,3 +136,37 @@ export const getAgentStudioErrorMessage = (error: Error): Error => {
return new Error(errorMessage); return new Error(errorMessage);
}; };
export const postAgentStudioFeedback = ({
agentId,
vote,
messageId,
appId,
apiKey,
abortSignal,
}: {
agentId: string;
vote: 0 | 1;
messageId: string;
appId: string;
apiKey: string;
abortSignal: AbortSignal;
}): Promise<Response> => {
const headers = new Headers();
headers.set('x-algolia-application-id', appId);
headers.set('x-algolia-api-key', apiKey);
headers.set('content-type', 'application/json');
const baseUrl = `${agentStudioBaseUrl(appId)}/feedback`;
return fetch(baseUrl, {
method: 'POST',
body: JSON.stringify({
messageId,
agentId,
vote,
}),
headers,
signal: abortSignal,
});
};

View file

@ -3,7 +3,13 @@ import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai'; import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai';
import { useCallback, useMemo, useRef } from 'react'; import { useCallback, useMemo, useRef } from 'react';
import { getAgentStudioErrorMessage, getValidToken, postFeedback } from './askai'; import {
agentStudioBaseUrl,
getAgentStudioErrorMessage,
getValidToken,
postAgentStudioFeedback,
postFeedback,
} from './askai';
import type { Exchange } from './AskAiScreen'; import type { Exchange } from './AskAiScreen';
import { ASK_AI_API_URL, BETA_ASK_AI_API_URL } from './constants'; import { ASK_AI_API_URL, BETA_ASK_AI_API_URL } from './constants';
import type { StoredSearchPlugin } from './stored-searches'; import type { StoredSearchPlugin } from './stored-searches';
@ -59,7 +65,7 @@ const getAgentStudioTransport = ({
searchParameters, searchParameters,
}: AgentStudioTransportParams): DefaultChatTransport<AIMessage> => { }: AgentStudioTransportParams): DefaultChatTransport<AIMessage> => {
return new DefaultChatTransport({ return new DefaultChatTransport({
api: `https://${appId}.algolia.net/agent-studio/1/agents/${assistantId}/completions?stream=true&compatibilityMode=ai-sdk-5`, api: `${agentStudioBaseUrl(appId)}/agents/${assistantId}/completions?stream=true&compatibilityMode=ai-sdk-5`,
headers: { headers: {
'x-algolia-application-id': appId, 'x-algolia-application-id': appId,
'x-algolia-api-key': apiKey, 'x-algolia-api-key': apiKey,
@ -145,19 +151,28 @@ export const useAskAi: UseAskAi = ({ assistantId, apiKey, appId, indexName, useS
async (messageId: string, thumbs: 0 | 1): Promise<void> => { async (messageId: string, thumbs: 0 | 1): Promise<void> => {
if (!assistantId) return; if (!assistantId) return;
const res = await postFeedback({ const res = await (params.agentStudio
assistantId, ? postAgentStudioFeedback({
thumbs, agentId: assistantId,
messageId, vote: thumbs,
appId, messageId,
abortSignal: abortControllerRef.current.signal, appId,
useStagingEnv, apiKey,
}); abortSignal: abortControllerRef.current.signal,
})
: postFeedback({
assistantId,
thumbs,
messageId,
appId,
abortSignal: abortControllerRef.current.signal,
useStagingEnv,
}));
if (res.status >= 300) throw new Error('Failed, try again later.'); if (res.status >= 300) throw new Error('Failed, try again later.');
conversations.addFeedback?.(messageId, thumbs === 1 ? 'like' : 'dislike'); conversations.addFeedback?.(messageId, thumbs === 1 ? 'like' : 'dislike');
}, },
[assistantId, appId, conversations, useStagingEnv], [assistantId, params.agentStudio, appId, apiKey, useStagingEnv, conversations],
); );
const onStopStreaming = async (): Promise<void> => { const onStopStreaming = async (): Promise<void> => {