From f83f5a911c84ec86dee3db167567679a092abd32 Mon Sep 17 00:00:00 2001 From: Dylan Tientcheu Date: Thu, 26 Mar 2026 12:32:15 +0100 Subject: [PATCH] feat: agent studio feedback integration (#2868) Cherry-picked from v5 branch. Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/demo-react/src/App.tsx | 12 ++++- .../src/examples/agent-studio-sidepanel.tsx | 25 +++++++++ .../demo-react/src/examples/agent-studio.tsx | 54 ++++++------------- packages/docsearch-react/src/AskAiScreen.tsx | 50 +++++++++-------- .../docsearch-react/src/DocSearchModal.tsx | 28 ++++------ .../src/Sidepanel/ConversationActions.tsx | 41 +++++++------- .../src/Sidepanel/ConversationScreen.tsx | 5 +- packages/docsearch-react/src/askai.ts | 36 +++++++++++++ packages/docsearch-react/src/useAskAi.ts | 37 +++++++++---- 9 files changed, 171 insertions(+), 117 deletions(-) create mode 100644 examples/demo-react/src/examples/agent-studio-sidepanel.tsx diff --git a/examples/demo-react/src/App.tsx b/examples/demo-react/src/App.tsx index 24447bfc..faebfb28 100644 --- a/examples/demo-react/src/App.tsx +++ b/examples/demo-react/src/App.tsx @@ -7,7 +7,8 @@ import './App.css'; import '@docsearch/css/dist/style.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 BasicAskAI from './examples/basic-askai'; import Composable from './examples/composable'; @@ -102,7 +103,14 @@ function App(): JSX.Element {

Agent Studio

- + +
+
+ +
+

Agent Studio sidepanel

+
+
diff --git a/examples/demo-react/src/examples/agent-studio-sidepanel.tsx b/examples/demo-react/src/examples/agent-studio-sidepanel.tsx new file mode 100644 index 00000000..667d1471 --- /dev/null +++ b/examples/demo-react/src/examples/agent-studio-sidepanel.tsx @@ -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 ( + + + + + ); +} diff --git a/examples/demo-react/src/examples/agent-studio.tsx b/examples/demo-react/src/examples/agent-studio.tsx index 6916e836..02efc55f 100644 --- a/examples/demo-react/src/examples/agent-studio.tsx +++ b/examples/demo-react/src/examples/agent-studio.tsx @@ -2,45 +2,25 @@ import { DocSearch } from '@docsearch/core'; import { DocSearchButton } from '@docsearch/modal/button'; import { DocSearchModal } from '@docsearch/modal/modal'; -import { SidepanelButton } from '@docsearch/sidepanel/button'; -import { Sidepanel } from '@docsearch/sidepanel/sidepanel'; import type { JSX } from 'react'; -export function AgentStudioExample(): JSX.Element { +export default function AgentStudio(): JSX.Element { return ( - <> - - - - - - - - - - + + + + ); } diff --git a/packages/docsearch-react/src/AskAiScreen.tsx b/packages/docsearch-react/src/AskAiScreen.tsx index 6c2aa2ce..24242bec 100644 --- a/packages/docsearch-react/src/AskAiScreen.tsx +++ b/packages/docsearch-react/src/AskAiScreen.tsx @@ -140,6 +140,8 @@ function AskAiExchangeCard({ isLastExchange && !displayParts.some((part) => part.type !== 'step-start'); + const messageId = agentStudio ? assistantMessage?.id || exchange.id : userMessage?.id || exchange.id; + return (
@@ -236,12 +238,11 @@ function AskAiExchangeCard({
@@ -262,7 +263,6 @@ interface AskAiScreenFooterActionsProps { translations: AskAiScreenTranslations; conversations: StoredSearchPlugin; onFeedback?: (messageId: string, thumbs: 0 | 1) => Promise; - agentStudio?: boolean; } export function AskAiScreenFooterActions({ @@ -272,7 +272,6 @@ export function AskAiScreenFooterActions({ translations, conversations, onFeedback, - agentStudio, }: AskAiScreenFooterActionsProps): JSX.Element | null { // local state for feedback, initialised from stored conversations const initialFeedback = React.useMemo(() => { @@ -310,26 +309,25 @@ export function AskAiScreenFooterActions({ return (
- {!agentStudio && - (feedback === null ? ( - <> - {saving ? ( - - ) : ( - <> - handleFeedback('like')} /> - handleFeedback('dislike')} /> - - )} - {savingError && ( -

{savingError.message || 'An error occured'}

- )} - - ) : ( -

- {thanksForFeedbackText} -

- ))} + {feedback === null ? ( + <> + {saving ? ( + + ) : ( + <> + handleFeedback('like')} /> + handleFeedback('dislike')} /> + + )} + {savingError && ( +

{savingError.message || 'An error occured'}

+ )} + + ) : ( +

+ {thanksForFeedbackText} +

+ )} navigator.clipboard.writeText(latestAssistantMessageContent)} @@ -373,7 +371,7 @@ export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps): startNewConversationButtonText = 'Start a new conversation', } = translations; - const { messages, askAiError, status } = props; + const { messages, askAiError, status, agentStudio } = props; // Check if there's a thread depth error const hasThreadDepthError = useMemo(() => { @@ -448,7 +446,7 @@ export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps): loadingStatus={props.status} translations={translations} conversations={props.conversations} - agentStudio={props.agentStudio} + agentStudio={agentStudio} onSearchQueryClick={handleSearchQueryClick} onFeedback={props.onFeedback} /> diff --git a/packages/docsearch-react/src/DocSearchModal.tsx b/packages/docsearch-react/src/DocSearchModal.tsx index 9fe5022d..3777b2f1 100644 --- a/packages/docsearch-react/src/DocSearchModal.tsx +++ b/packages/docsearch-react/src/DocSearchModal.tsx @@ -20,13 +20,12 @@ import type { ScreenStateTranslations } from './ScreenState'; import { ScreenState } from './ScreenState'; import type { SearchBoxTranslations } from './SearchBox'; import { SearchBox } from './SearchBox'; -import { createStoredConversations, createStoredSearches } from './stored-searches'; +import { createStoredSearches } from './stored-searches'; import type { DocSearchHit, DocSearchState, InternalDocSearchHit, StoredAskAiMessage, - StoredAskAiState, StoredDocSearchHit, SuggestedQuestionHit, } from './types'; @@ -384,12 +383,6 @@ export function DocSearchModal({ const defaultIndexName = indexes[0].name; // storage - const conversations = React.useRef( - createStoredConversations({ - key: `__DOCSEARCH_ASKAI_CONVERSATIONS__${askAiConfig?.indexName || defaultIndexName}`, - limit: 10, - }), - ).current; const favoriteSearches = React.useRef( createStoredSearches({ key: `__DOCSEARCH_FAVORITE_SEARCHES__${defaultIndexName}`, @@ -405,15 +398,16 @@ export function DocSearchModal({ const [stoppedStream, setStoppedStream] = React.useState(false); - const { messages, status, setMessages, sendMessage, stopAskAiStreaming, askAiError, sendFeedback } = useAskAi({ - assistantId: askAiConfigurationId, - apiKey: askAiConfig?.apiKey || apiKey, - appId: askAiConfig?.appId || appId, - indexName: askAiConfig?.indexName || defaultIndexName, - searchParameters: askAiSearchParameters, - useStagingEnv: askAiUseStagingEnv, - agentStudio, - }); + 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 prevStatus = React.useRef(status); React.useEffect(() => { diff --git a/packages/docsearch-react/src/Sidepanel/ConversationActions.tsx b/packages/docsearch-react/src/Sidepanel/ConversationActions.tsx index 22c6a167..e8864a57 100644 --- a/packages/docsearch-react/src/Sidepanel/ConversationActions.tsx +++ b/packages/docsearch-react/src/Sidepanel/ConversationActions.tsx @@ -14,7 +14,6 @@ interface ConversationActionsProps { conversations: StoredSearchPlugin; onFeedback?: (messageId: string, thumbs: 0 | 1) => Promise; isSidepanel?: boolean; - agentStudio?: boolean; } export function ConversationActions({ @@ -24,7 +23,6 @@ export function ConversationActions({ onFeedback, latestAssistantMessageContent, showActions, - agentStudio, }: ConversationActionsProps): JSX.Element | null { const initialFeedback = React.useMemo(() => { const message = conversations.getOne?.(id); @@ -65,26 +63,25 @@ export function ConversationActions({ translations={translations} onClick={() => navigator.clipboard.writeText(latestAssistantMessageContent)} /> - {!agentStudio && - (feedback === null ? ( - <> - {saving ? ( - - ) : ( - <> - handleFeedback('like')} /> - handleFeedback('dislike')} /> - - )} - {savingError && ( -

{savingError.message || 'An error occured'}

- )} - - ) : ( -

- {thanksForFeedbackText} -

- ))} + {feedback === null ? ( + <> + {saving ? ( + + ) : ( + <> + handleFeedback('like')} /> + handleFeedback('dislike')} /> + + )} + {savingError && ( +

{savingError.message || 'An error occured'}

+ )} + + ) : ( +

+ {thanksForFeedbackText} +

+ )}
); } diff --git a/packages/docsearch-react/src/Sidepanel/ConversationScreen.tsx b/packages/docsearch-react/src/Sidepanel/ConversationScreen.tsx index 3a18589e..b12b3e93 100644 --- a/packages/docsearch-react/src/Sidepanel/ConversationScreen.tsx +++ b/packages/docsearch-react/src/Sidepanel/ConversationScreen.tsx @@ -120,6 +120,8 @@ const ConversationExchange = React.forwardRef
@@ -212,12 +214,11 @@ const ConversationExchange = React.forwardRef
diff --git a/packages/docsearch-react/src/askai.ts b/packages/docsearch-react/src/askai.ts index 809e24a5..693c0d0f 100644 --- a/packages/docsearch-react/src/askai.ts +++ b/packages/docsearch-react/src/askai.ts @@ -3,6 +3,8 @@ import { ASK_AI_API_URL, BETA_ASK_AI_API_URL } from './constants'; // ... existing imports ... const TOKEN_KEY = 'askai_token'; +export const agentStudioBaseUrl = (appId: string): string => `https://${appId}.algolia.net/agent-studio/1`; + type TokenPayload = { exp: number }; const decode = (token: string): TokenPayload => { @@ -134,3 +136,37 @@ export const getAgentStudioErrorMessage = (error: Error): Error => { 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 => { + 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, + }); +}; diff --git a/packages/docsearch-react/src/useAskAi.ts b/packages/docsearch-react/src/useAskAi.ts index a30b6d49..c77eab9c 100644 --- a/packages/docsearch-react/src/useAskAi.ts +++ b/packages/docsearch-react/src/useAskAi.ts @@ -3,7 +3,13 @@ import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai'; 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 { ASK_AI_API_URL, BETA_ASK_AI_API_URL } from './constants'; import type { StoredSearchPlugin } from './stored-searches'; @@ -59,7 +65,7 @@ const getAgentStudioTransport = ({ searchParameters, }: AgentStudioTransportParams): 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: { 'x-algolia-application-id': appId, '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 => { if (!assistantId) return; - const res = await postFeedback({ - assistantId, - thumbs, - messageId, - appId, - abortSignal: abortControllerRef.current.signal, - useStagingEnv, - }); + const res = await (params.agentStudio + ? postAgentStudioFeedback({ + agentId: assistantId, + vote: thumbs, + messageId, + appId, + 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.'); conversations.addFeedback?.(messageId, thumbs === 1 ? 'like' : 'dislike'); }, - [assistantId, appId, conversations, useStagingEnv], + [assistantId, params.agentStudio, appId, apiKey, useStagingEnv, conversations], ); const onStopStreaming = async (): Promise => {