Skip to content

Commit eb1bd3a

Browse files
authored
AI response feedback buttons (#3482)
1 parent b7284b0 commit eb1bd3a

File tree

24 files changed

+204
-43
lines changed

24 files changed

+204
-43
lines changed

.changeset/hungry-bears-vanish.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gitbook": patch
3+
---
4+
5+
AI response feedback buttons

bun.lock

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
},
4949
"packages/gitbook": {
5050
"name": "gitbook",
51-
"version": "0.13.1",
51+
"version": "0.14.0",
5252
"dependencies": {
5353
"@gitbook/api": "catalog:",
5454
"@gitbook/cache-tags": "workspace:*",
@@ -146,7 +146,7 @@
146146
},
147147
"packages/icons": {
148148
"name": "@gitbook/icons",
149-
"version": "0.2.0",
149+
"version": "0.2.1",
150150
"bin": {
151151
"gitbook-icons": "./bin/gitbook-icons.js",
152152
},
@@ -166,7 +166,7 @@
166166
},
167167
"packages/openapi-parser": {
168168
"name": "@gitbook/openapi-parser",
169-
"version": "2.2.0",
169+
"version": "2.2.1",
170170
"dependencies": {
171171
"@scalar/openapi-parser": "^0.18.0",
172172
"@scalar/openapi-types": "^0.1.9",
@@ -181,7 +181,7 @@
181181
},
182182
"packages/react-contentkit": {
183183
"name": "@gitbook/react-contentkit",
184-
"version": "0.7.1",
184+
"version": "0.7.2",
185185
"dependencies": {
186186
"@gitbook/api": "catalog:",
187187
"@gitbook/icons": "workspace:*",
@@ -213,7 +213,7 @@
213213
},
214214
"packages/react-openapi": {
215215
"name": "@gitbook/react-openapi",
216-
"version": "1.3.2",
216+
"version": "1.3.3",
217217
"dependencies": {
218218
"@gitbook/openapi-parser": "workspace:*",
219219
"@scalar/api-client-react": "^1.3.16",
@@ -246,7 +246,7 @@
246246
"react-dom": "^19.0.0",
247247
},
248248
"catalog": {
249-
"@gitbook/api": "^0.128.0",
249+
"@gitbook/api": "^0.129.0",
250250
},
251251
"packages": {
252252
"@ai-sdk/provider": ["@ai-sdk/[email protected]", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="],
@@ -609,7 +609,7 @@
609609

610610
"@fortawesome/fontawesome-svg-core": ["@fortawesome/[email protected]", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
611611

612-
"@gitbook/api": ["@gitbook/api@0.128.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-VO98hRGfUcFdwMplvFW49jz72/ew2waE7RCu+URKAY2AyPHAduv2zgluF5gF1VcDyuJM9PKFtsdsMtpUjI5sYg=="],
612+
"@gitbook/api": ["@gitbook/api@0.129.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-Uh+k/BiDgdXj5a8BIlvwfEXdZNNlQMFDlGKmivUYH1gvFySqrTO6PjR/HtUOZEdNauAeWZmGoB8CLuwrVZS+YA=="],
613613

614614
"@gitbook/cache-tags": ["@gitbook/cache-tags@workspace:packages/cache-tags"],
615615

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"workspaces": {
3535
"packages": ["packages/*"],
3636
"catalog": {
37-
"@gitbook/api": "^0.128.0"
37+
"@gitbook/api": "^0.129.0"
3838
}
3939
},
4040
"patchedDependencies": {

packages/cache-tags/src/index.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export function getCacheTag(
9090
| {
9191
tag: 'translation';
9292
organization: string;
93-
translationSettings: string;
93+
translation: string;
9494
}
9595
): string {
9696
switch (spec.tag) {
@@ -115,7 +115,7 @@ export function getCacheTag(
115115
case 'openapi':
116116
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
117117
case 'translation':
118-
return `organization:${spec.organization}:translation:${spec.translationSettings}`;
118+
return `organization:${spec.organization}:translation:${spec.translation}`;
119119
default:
120120
assertNever(spec);
121121
}
@@ -144,6 +144,10 @@ export function getComputedContentSourceCacheTags(
144144
) {
145145
const tags: string[] = [];
146146

147+
if (!('dependencies' in source)) {
148+
return tags;
149+
}
150+
147151
// We add the dependencies as tags, to ensure that the computed content is invalidated
148152
// when the dependencies are updated.
149153
const dependencies = Object.values(source.dependencies ?? {});
@@ -167,12 +171,12 @@ export function getComputedContentSourceCacheTags(
167171
})
168172
);
169173
break;
170-
case 'translation-language':
174+
case 'translation':
171175
tags.push(
172176
getCacheTag({
173177
tag: 'translation',
174178
organization: inContext.organizationId,
175-
translationSettings: dependency.ref.translationSettings,
179+
translation: dependency.ref.translation,
176180
})
177181
);
178182
break;

packages/gitbook/e2e/util.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -318,10 +318,6 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
318318
feedback: {
319319
enabled: false,
320320
},
321-
// TODO: remove aiSearch once the cache has been fully updated (after 11/07/2025)
322-
aiSearch: {
323-
enabled: true,
324-
},
325321
ai: {
326322
mode: CustomizationAIMode.None,
327323
},
@@ -337,6 +333,10 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
337333
pagination: {
338334
enabled: true,
339335
},
336+
pageActions: {
337+
externalAI: true,
338+
markdown: true,
339+
},
340340
trademark: {
341341
enabled: true,
342342
},

packages/gitbook/src/components/AI/useAIChat.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useAIMessageContextRef } from './useAIMessageContext';
1111
export type AIChatMessage = {
1212
role: AIMessageRole;
1313
content: React.ReactNode;
14+
query?: string;
1415
};
1516

1617
export type AIChatState = {
@@ -24,6 +25,11 @@ export type AIChatState = {
2425
*/
2526
responseId: string | null;
2627

28+
/**
29+
* The latest query sent to the AI.
30+
*/
31+
query: string | null;
32+
2733
/**
2834
* Messages in the session.
2935
*/
@@ -73,6 +79,7 @@ const globalState = zustand.create<{
7379
opened: false,
7480
responseId: null,
7581
messages: [],
82+
query: null,
7683
followUpSuggestions: [],
7784
loading: false,
7885
error: false,
@@ -106,6 +113,7 @@ export function useAIChatController(): AIChatController {
106113
opened: state.opened,
107114
loading: false,
108115
messages: [],
116+
query: null,
109117
followUpSuggestions: [],
110118
responseId: null,
111119
error: false,
@@ -128,6 +136,7 @@ export function useAIChatController(): AIChatController {
128136
content: null,
129137
},
130138
],
139+
query: input.message,
131140
followUpSuggestions: [],
132141
loading: true,
133142
error: false,

packages/gitbook/src/components/AIChat/AIChatMessages.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { tcls } from '@/lib/tailwind';
22
import { AIMessageRole } from '@gitbook/api';
33
import type React from 'react';
44
import type { AIChatController, AIChatState } from '../AI/useAIChat';
5+
import { AIResponseFeedback } from './AIResponseFeedback';
56
import { AIChatFollowupSuggestions } from './AiChatFollowupSuggestions';
67

78
export function AIChatMessages(props: {
@@ -57,10 +58,19 @@ export function AIChatMessages(props: {
5758
) : null}
5859

5960
{isLastMessage ? (
60-
<AIChatFollowupSuggestions
61-
chat={chat}
62-
chatController={chatController}
63-
/>
61+
<>
62+
{!chat.loading && !chat.error && chat.query && chat.responseId && (
63+
<AIResponseFeedback
64+
responseId={chat.responseId}
65+
query={chat.query}
66+
className="-ml-1 -mt-4"
67+
/>
68+
)}
69+
<AIChatFollowupSuggestions
70+
chat={chat}
71+
chatController={chatController}
72+
/>
73+
</>
6474
) : null}
6575
</div>
6676
);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use client';
2+
3+
import { useLanguage } from '@/intl/client';
4+
import { t, tString } from '@/intl/translate';
5+
import { type ClassValue, tcls } from '@/lib/tailwind';
6+
import { useState } from 'react';
7+
import { useTrackEvent } from '../Insights';
8+
import { Button } from '../primitives';
9+
10+
export function AIResponseFeedback(props: {
11+
className?: ClassValue;
12+
responseId: string;
13+
query: string;
14+
}) {
15+
const { className, responseId, query } = props;
16+
17+
const language = useLanguage();
18+
const [rating, setRating] = useState<1 | -1 | null>(null);
19+
const trackEvent = useTrackEvent();
20+
21+
const handleRating = (rating: 1 | -1) => {
22+
setRating(rating);
23+
trackEvent({ type: 'ask_rate_response', query, responseId, rating });
24+
};
25+
26+
return (
27+
<div className={tcls('flex h-fit items-center', className)}>
28+
<Button
29+
icon="thumbs-up"
30+
iconOnly
31+
label={tString(language, 'was_this_helpful_positive_label')}
32+
variant="blank"
33+
className={tcls(
34+
'animate-fadeIn overflow-hidden text-tint-subtle transition-all',
35+
rating !== null && rating !== 1 && 'px-0 text-[0] opacity-0'
36+
)}
37+
size="medium"
38+
style={{ animationDuration: '.5s' }}
39+
onClick={() => handleRating(1)}
40+
disabled={rating !== null}
41+
active={rating === 1}
42+
key="positive"
43+
/>
44+
<Button
45+
icon="thumbs-down"
46+
iconOnly
47+
label={tString(language, 'was_this_helpful_negative_label')}
48+
variant="blank"
49+
className={tcls(
50+
'animate-fadeIn overflow-hidden text-tint-subtle transition-all',
51+
rating !== null && rating !== -1 && 'px-0 text-[0] opacity-0'
52+
)}
53+
size="medium"
54+
style={{ animationDelay: '.2s', animationDuration: '.5s' }}
55+
onClick={() => handleRating(-1)}
56+
disabled={rating !== null}
57+
active={rating === -1}
58+
key="negative"
59+
/>
60+
{rating !== null ? (
61+
<span
62+
className="ml-2 animate-fadeIn text-tint-subtle"
63+
style={{ animationDelay: '.3s', animationDuration: '.5s' }}
64+
>
65+
{t(language, 'was_this_helpful_thank_you')}
66+
</span>
67+
) : null}
68+
</div>
69+
);
70+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './AIChat';
22
export * from './AIChatButton';
33
export * from './AIChatIcon';
4+
export * from './AIResponseFeedback';

packages/gitbook/src/components/Search/SearchAskAnswer.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { t } from '@/intl/translate';
1010
import type { TranslationLanguage } from '@/intl/translations';
1111
import { tcls } from '@/lib/tailwind';
1212

13+
import { AIResponseFeedback } from '../AIChat';
1314
import { useTrackEvent } from '../Insights';
1415
import { Link } from '../primitives';
1516
import { useSearchAskContext } from './SearchAskContext';
@@ -97,7 +98,11 @@ export function SearchAskAnswer(props: { query: string }) {
9798
<div className="flex min-h-full p-4">
9899
{askState?.type === 'answer' ? (
99100
<React.Suspense fallback={loading}>
100-
<TransitionAnswerBody answer={askState.answer} placeholder={loading} />
101+
<TransitionAnswerBody
102+
answer={askState.answer}
103+
placeholder={loading}
104+
query={query}
105+
/>
101106
</React.Suspense>
102107
) : null}
103108
{askState?.type === 'error' ? (
@@ -114,8 +119,12 @@ export function SearchAskAnswer(props: { query: string }) {
114119
* Since the answer can be an async component that could suspend rendering,
115120
* we need to wrap it in a transition to avoid flickering.
116121
*/
117-
function TransitionAnswerBody(props: { answer: AskAnswerResult; placeholder: React.ReactNode }) {
118-
const { answer, placeholder } = props;
122+
function TransitionAnswerBody(props: {
123+
query: string;
124+
answer: AskAnswerResult;
125+
placeholder: React.ReactNode;
126+
}) {
127+
const { query, answer, placeholder } = props;
119128
const [display, setDisplay] = React.useState<AskAnswerResult | null>(null);
120129
const [_isPending, startTransition] = React.useTransition();
121130

@@ -127,21 +136,25 @@ function TransitionAnswerBody(props: { answer: AskAnswerResult; placeholder: Rea
127136

128137
return display ? (
129138
<div className={tcls('w-full')}>
130-
<AnswerBody answer={display} />
139+
<AnswerBody query={query} answer={display} />
131140
</div>
132141
) : (
133142
<>{placeholder}</>
134143
);
135144
}
136145

137-
function AnswerBody(props: { answer: AskAnswerResult }) {
138-
const { answer } = props;
146+
function AnswerBody(props: { query: string; answer: AskAnswerResult }) {
147+
const { query, answer } = props;
139148
const language = useLanguage();
140149

141150
return (
142151
<>
143152
<div data-testid="search-ask-answer" className="text-tint-strong">
144153
{answer.body ?? t(language, 'search_ask_no_answer')}
154+
{answer.sources.length > 0 ? (
155+
// @TODO: Add responseId once search uses new AI endpoint
156+
<AIResponseFeedback query={query} className="-ml-1 mt-2" responseId="" />
157+
) : null}
145158
{answer.followupQuestions.length > 0 ? (
146159
<AnswerFollowupQuestions followupQuestions={answer.followupQuestions} />
147160
) : null}
@@ -162,7 +175,7 @@ function AnswerFollowupQuestions(props: { followupQuestions: string[] }) {
162175
const getSearchLinkProps = useSearchLink();
163176

164177
return (
165-
<div className={tcls('flex', 'flex-col', 'flex-wrap', 'mt-4', 'sm:mt-6')}>
178+
<div className={tcls('flex', 'flex-col', 'flex-wrap', 'mt-4')}>
166179
{followupQuestions.map((question) => (
167180
<Link
168181
key={question}

0 commit comments

Comments
 (0)