From eb1bd3aa014916a88ccd7030aac21019cc8af854 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Fri, 18 Jul 2025 17:40:04 +0200 Subject: [PATCH] AI response feedback buttons (#3482) --- .changeset/hungry-bears-vanish.md | 5 ++ bun.lock | 14 ++-- package.json | 2 +- packages/cache-tags/src/index.ts | 12 ++-- packages/gitbook/e2e/util.ts | 8 +-- .../gitbook/src/components/AI/useAIChat.tsx | 9 +++ .../src/components/AIChat/AIChatMessages.tsx | 18 +++-- .../components/AIChat/AIResponseFeedback.tsx | 70 +++++++++++++++++++ .../gitbook/src/components/AIChat/index.ts | 1 + .../src/components/Search/SearchAskAnswer.tsx | 27 +++++-- .../src/components/primitives/Button.tsx | 26 ++++++- .../src/components/primitives/styles.ts | 7 +- .../src/components/utils/isAIChatEnabled.ts | 4 +- packages/gitbook/src/intl/translations/de.ts | 2 + packages/gitbook/src/intl/translations/en.ts | 2 + packages/gitbook/src/intl/translations/es.ts | 2 + packages/gitbook/src/intl/translations/fr.ts | 2 + packages/gitbook/src/intl/translations/ja.ts | 2 + packages/gitbook/src/intl/translations/nl.ts | 2 + packages/gitbook/src/intl/translations/no.ts | 2 + .../gitbook/src/intl/translations/pt-br.ts | 2 + packages/gitbook/src/intl/translations/zh.ts | 2 + packages/gitbook/src/lib/pages.test.ts | 18 ++++- packages/gitbook/src/lib/utils.ts | 8 +-- 24 files changed, 204 insertions(+), 43 deletions(-) create mode 100644 .changeset/hungry-bears-vanish.md create mode 100644 packages/gitbook/src/components/AIChat/AIResponseFeedback.tsx diff --git a/.changeset/hungry-bears-vanish.md b/.changeset/hungry-bears-vanish.md new file mode 100644 index 000000000..010f067c7 --- /dev/null +++ b/.changeset/hungry-bears-vanish.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +AI response feedback buttons diff --git a/bun.lock b/bun.lock index fdfb6880a..156aa4b34 100644 --- a/bun.lock +++ b/bun.lock @@ -48,7 +48,7 @@ }, "packages/gitbook": { "name": "gitbook", - "version": "0.13.1", + "version": "0.14.0", "dependencies": { "@gitbook/api": "catalog:", "@gitbook/cache-tags": "workspace:*", @@ -146,7 +146,7 @@ }, "packages/icons": { "name": "@gitbook/icons", - "version": "0.2.0", + "version": "0.2.1", "bin": { "gitbook-icons": "./bin/gitbook-icons.js", }, @@ -166,7 +166,7 @@ }, "packages/openapi-parser": { "name": "@gitbook/openapi-parser", - "version": "2.2.0", + "version": "2.2.1", "dependencies": { "@scalar/openapi-parser": "^0.18.0", "@scalar/openapi-types": "^0.1.9", @@ -181,7 +181,7 @@ }, "packages/react-contentkit": { "name": "@gitbook/react-contentkit", - "version": "0.7.1", + "version": "0.7.2", "dependencies": { "@gitbook/api": "catalog:", "@gitbook/icons": "workspace:*", @@ -213,7 +213,7 @@ }, "packages/react-openapi": { "name": "@gitbook/react-openapi", - "version": "1.3.2", + "version": "1.3.3", "dependencies": { "@gitbook/openapi-parser": "workspace:*", "@scalar/api-client-react": "^1.3.16", @@ -246,7 +246,7 @@ "react-dom": "^19.0.0", }, "catalog": { - "@gitbook/api": "^0.128.0", + "@gitbook/api": "^0.129.0", }, "packages": { "@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="], @@ -609,7 +609,7 @@ "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="], - "@gitbook/api": ["@gitbook/api@0.128.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-VO98hRGfUcFdwMplvFW49jz72/ew2waE7RCu+URKAY2AyPHAduv2zgluF5gF1VcDyuJM9PKFtsdsMtpUjI5sYg=="], + "@gitbook/api": ["@gitbook/api@0.129.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-Uh+k/BiDgdXj5a8BIlvwfEXdZNNlQMFDlGKmivUYH1gvFySqrTO6PjR/HtUOZEdNauAeWZmGoB8CLuwrVZS+YA=="], "@gitbook/cache-tags": ["@gitbook/cache-tags@workspace:packages/cache-tags"], diff --git a/package.json b/package.json index e98bb9a83..b08f09e15 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "workspaces": { "packages": ["packages/*"], "catalog": { - "@gitbook/api": "^0.128.0" + "@gitbook/api": "^0.129.0" } }, "patchedDependencies": { diff --git a/packages/cache-tags/src/index.ts b/packages/cache-tags/src/index.ts index a6d937792..f4350b717 100644 --- a/packages/cache-tags/src/index.ts +++ b/packages/cache-tags/src/index.ts @@ -90,7 +90,7 @@ export function getCacheTag( | { tag: 'translation'; organization: string; - translationSettings: string; + translation: string; } ): string { switch (spec.tag) { @@ -115,7 +115,7 @@ export function getCacheTag( case 'openapi': return `organization:${spec.organization}:openapi:${spec.openAPISpec}`; case 'translation': - return `organization:${spec.organization}:translation:${spec.translationSettings}`; + return `organization:${spec.organization}:translation:${spec.translation}`; default: assertNever(spec); } @@ -144,6 +144,10 @@ export function getComputedContentSourceCacheTags( ) { const tags: string[] = []; + if (!('dependencies' in source)) { + return tags; + } + // We add the dependencies as tags, to ensure that the computed content is invalidated // when the dependencies are updated. const dependencies = Object.values(source.dependencies ?? {}); @@ -167,12 +171,12 @@ export function getComputedContentSourceCacheTags( }) ); break; - case 'translation-language': + case 'translation': tags.push( getCacheTag({ tag: 'translation', organization: inContext.organizationId, - translationSettings: dependency.ref.translationSettings, + translation: dependency.ref.translation, }) ); break; diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts index 22e6b2e1f..a38cc94f6 100644 --- a/packages/gitbook/e2e/util.ts +++ b/packages/gitbook/e2e/util.ts @@ -318,10 +318,6 @@ export function getCustomizationURL(partial: DeepPartial + <> + {!chat.loading && !chat.error && chat.query && chat.responseId && ( + + )} + + ) : null} ); diff --git a/packages/gitbook/src/components/AIChat/AIResponseFeedback.tsx b/packages/gitbook/src/components/AIChat/AIResponseFeedback.tsx new file mode 100644 index 000000000..4bef9b07d --- /dev/null +++ b/packages/gitbook/src/components/AIChat/AIResponseFeedback.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { useLanguage } from '@/intl/client'; +import { t, tString } from '@/intl/translate'; +import { type ClassValue, tcls } from '@/lib/tailwind'; +import { useState } from 'react'; +import { useTrackEvent } from '../Insights'; +import { Button } from '../primitives'; + +export function AIResponseFeedback(props: { + className?: ClassValue; + responseId: string; + query: string; +}) { + const { className, responseId, query } = props; + + const language = useLanguage(); + const [rating, setRating] = useState<1 | -1 | null>(null); + const trackEvent = useTrackEvent(); + + const handleRating = (rating: 1 | -1) => { + setRating(rating); + trackEvent({ type: 'ask_rate_response', query, responseId, rating }); + }; + + return ( +
+
+ ); +} diff --git a/packages/gitbook/src/components/AIChat/index.ts b/packages/gitbook/src/components/AIChat/index.ts index 28fbd9193..0c0dbd093 100644 --- a/packages/gitbook/src/components/AIChat/index.ts +++ b/packages/gitbook/src/components/AIChat/index.ts @@ -1,3 +1,4 @@ export * from './AIChat'; export * from './AIChatButton'; export * from './AIChatIcon'; +export * from './AIResponseFeedback'; diff --git a/packages/gitbook/src/components/Search/SearchAskAnswer.tsx b/packages/gitbook/src/components/Search/SearchAskAnswer.tsx index 72c95ed34..adceedfae 100644 --- a/packages/gitbook/src/components/Search/SearchAskAnswer.tsx +++ b/packages/gitbook/src/components/Search/SearchAskAnswer.tsx @@ -10,6 +10,7 @@ import { t } from '@/intl/translate'; import type { TranslationLanguage } from '@/intl/translations'; import { tcls } from '@/lib/tailwind'; +import { AIResponseFeedback } from '../AIChat'; import { useTrackEvent } from '../Insights'; import { Link } from '../primitives'; import { useSearchAskContext } from './SearchAskContext'; @@ -97,7 +98,11 @@ export function SearchAskAnswer(props: { query: string }) {
{askState?.type === 'answer' ? ( - + ) : null} {askState?.type === 'error' ? ( @@ -114,8 +119,12 @@ export function SearchAskAnswer(props: { query: string }) { * Since the answer can be an async component that could suspend rendering, * we need to wrap it in a transition to avoid flickering. */ -function TransitionAnswerBody(props: { answer: AskAnswerResult; placeholder: React.ReactNode }) { - const { answer, placeholder } = props; +function TransitionAnswerBody(props: { + query: string; + answer: AskAnswerResult; + placeholder: React.ReactNode; +}) { + const { query, answer, placeholder } = props; const [display, setDisplay] = React.useState(null); const [_isPending, startTransition] = React.useTransition(); @@ -127,21 +136,25 @@ function TransitionAnswerBody(props: { answer: AskAnswerResult; placeholder: Rea return display ? (
- +
) : ( <>{placeholder} ); } -function AnswerBody(props: { answer: AskAnswerResult }) { - const { answer } = props; +function AnswerBody(props: { query: string; answer: AskAnswerResult }) { + const { query, answer } = props; const language = useLanguage(); return ( <>
{answer.body ?? t(language, 'search_ask_no_answer')} + {answer.sources.length > 0 ? ( + // @TODO: Add responseId once search uses new AI endpoint + + ) : null} {answer.followupQuestions.length > 0 ? ( ) : null} @@ -162,7 +175,7 @@ function AnswerFollowupQuestions(props: { followupQuestions: string[] }) { const getSearchLinkProps = useSearchLink(); return ( -
+
{followupQuestions.map((question) => ( { const sizes = { default: ['text-base', 'font-semibold', 'px-5', 'py-2', 'circular-corners:px-6'], - medium: ['text-sm', 'px-3.5', 'py-1.5', 'circular-corners:px-4'], + medium: ['text-sm', iconOnly ? 'px-2' : 'px-3.5', 'py-1.5', 'circular-corners:px-4'], small: ['text-xs', 'py-2', iconOnly ? 'px-2' : 'px-3'], xsmall: ['text-xs', 'py-1', iconOnly ? 'px-1.5' : 'px-2'], }; @@ -142,6 +149,7 @@ export const Button = React.forwardRef< classNames={['ButtonStyles']} insights={insights} aria-label={label?.toString()} + aria-pressed={active === undefined ? undefined : active} target={target} {...rest} > @@ -156,12 +164,24 @@ export const Button = React.forwardRef< type="button" className={tcls(buttonOnlyClassNames, domClassName)} aria-label={label?.toString()} + aria-pressed={active === undefined ? undefined : active} + disabled={disabled} {...rest} > {content} ); - return iconOnly && label ? {button} : button; + return iconOnly && label ? ( + + {button} + + ) : ( + button + ); } ); diff --git a/packages/gitbook/src/components/primitives/styles.ts b/packages/gitbook/src/components/primitives/styles.ts index 85ebd5055..6d84e4770 100644 --- a/packages/gitbook/src/components/primitives/styles.ts +++ b/packages/gitbook/src/components/primitives/styles.ts @@ -33,12 +33,9 @@ export const ButtonStyles = [ 'shrink-0', 'truncate', - 'disabled:opacity-50', 'disabled:cursor-not-allowed', - 'disabled:bg-tint', - 'disabled:text-tint/8', - 'disabled:shadow-none', - 'disabled:hover:scale-100', + 'disabled:!translate-y-0', + 'disabled:!shadow-none', ] as ClassValue[]; export const CardStyles = [ diff --git a/packages/gitbook/src/components/utils/isAIChatEnabled.ts b/packages/gitbook/src/components/utils/isAIChatEnabled.ts index d7e65a275..c23b60181 100644 --- a/packages/gitbook/src/components/utils/isAIChatEnabled.ts +++ b/packages/gitbook/src/components/utils/isAIChatEnabled.ts @@ -1,7 +1,5 @@ import type { GitBookSiteContext } from '@/lib/context'; import { CustomizationAIMode } from '@gitbook/api'; -// TODO: remove aiSearch and optional chain once the cache has been fully updated (after 11/07/2025) export const isAIChatEnabled = (context: GitBookSiteContext) => - context.customization.ai?.mode === CustomizationAIMode.Assistant && - (context.site.id === 'site_p4Xo4' || context.site.id === 'site_JOVzv'); + context.customization.ai.mode === CustomizationAIMode.Assistant; diff --git a/packages/gitbook/src/intl/translations/de.ts b/packages/gitbook/src/intl/translations/de.ts index a46f6730a..48bfac52c 100644 --- a/packages/gitbook/src/intl/translations/de.ts +++ b/packages/gitbook/src/intl/translations/de.ts @@ -32,6 +32,8 @@ export const de = { was_this_helpful_negative: 'Nein', was_this_helpful_thank_you: 'Danke!', was_this_helpful_comment: 'Möchten Sie etwas hinzufügen?', + was_this_helpful_positive_label: 'Hilfreich', + was_this_helpful_negative_label: 'Nicht hilfreich', submit: 'Absenden', annotation_button_label: 'Kommentar öffnen', code_copied: 'Kopiert!', diff --git a/packages/gitbook/src/intl/translations/en.ts b/packages/gitbook/src/intl/translations/en.ts index c1b24bbd7..92dc6e72f 100644 --- a/packages/gitbook/src/intl/translations/en.ts +++ b/packages/gitbook/src/intl/translations/en.ts @@ -32,6 +32,8 @@ export const en = { was_this_helpful_negative: 'No', was_this_helpful_thank_you: 'Thank you!', was_this_helpful_comment: "Anything you'd like to add?", + was_this_helpful_positive_label: 'Helpful', + was_this_helpful_negative_label: 'Not helpful', submit: 'Submit', annotation_button_label: 'Open annotation', code_copied: 'Copied!', diff --git a/packages/gitbook/src/intl/translations/es.ts b/packages/gitbook/src/intl/translations/es.ts index 3b05a5a75..34c44c9f6 100644 --- a/packages/gitbook/src/intl/translations/es.ts +++ b/packages/gitbook/src/intl/translations/es.ts @@ -34,6 +34,8 @@ export const es: TranslationLanguage = { was_this_helpful_negative: 'No', was_this_helpful_thank_you: '¡Gracias!', was_this_helpful_comment: '¿Algo más que te gustaría añadir?', + was_this_helpful_positive_label: 'Útil', + was_this_helpful_negative_label: 'No útil', submit: 'Enviar', annotation_button_label: 'Abrir anotación', code_copied: '¡Copiado!', diff --git a/packages/gitbook/src/intl/translations/fr.ts b/packages/gitbook/src/intl/translations/fr.ts index 40fb08b2e..f4759604f 100644 --- a/packages/gitbook/src/intl/translations/fr.ts +++ b/packages/gitbook/src/intl/translations/fr.ts @@ -34,6 +34,8 @@ export const fr: TranslationLanguage = { was_this_helpful_negative: 'Non', was_this_helpful_thank_you: 'Merci!', was_this_helpful_comment: 'Quelque chose à ajouter?', + was_this_helpful_positive_label: 'Utile', + was_this_helpful_negative_label: 'Pas utile', submit: 'Soumettre', annotation_button_label: "Ouvrir l'annotation", code_copied: 'Copié !', diff --git a/packages/gitbook/src/intl/translations/ja.ts b/packages/gitbook/src/intl/translations/ja.ts index 0dc9b500e..1116efc21 100644 --- a/packages/gitbook/src/intl/translations/ja.ts +++ b/packages/gitbook/src/intl/translations/ja.ts @@ -34,6 +34,8 @@ export const ja: TranslationLanguage = { was_this_helpful_negative: 'いいえ', was_this_helpful_thank_you: 'ありがとうございます!', was_this_helpful_comment: '何か追加したいことはありますか?', + was_this_helpful_positive_label: '役立つ', + was_this_helpful_negative_label: '役立たない', submit: '送信', annotation_button_label: '注釈を開く', code_copied: 'コピーしました!', diff --git a/packages/gitbook/src/intl/translations/nl.ts b/packages/gitbook/src/intl/translations/nl.ts index a635ddd6a..810054d33 100644 --- a/packages/gitbook/src/intl/translations/nl.ts +++ b/packages/gitbook/src/intl/translations/nl.ts @@ -34,6 +34,8 @@ export const nl: TranslationLanguage = { was_this_helpful_negative: 'Nee', was_this_helpful_thank_you: 'Bedankt!', was_this_helpful_comment: 'Nog iets toe te voegen?', + was_this_helpful_positive_label: 'Nuttig', + was_this_helpful_negative_label: 'Niet nuttig', submit: 'Versturen', annotation_button_label: 'Open annotatie', code_copied: 'Gekopieerd!', diff --git a/packages/gitbook/src/intl/translations/no.ts b/packages/gitbook/src/intl/translations/no.ts index 198848ef0..04ccf1c5a 100644 --- a/packages/gitbook/src/intl/translations/no.ts +++ b/packages/gitbook/src/intl/translations/no.ts @@ -34,6 +34,8 @@ export const no: TranslationLanguage = { was_this_helpful_negative: 'Nei', was_this_helpful_thank_you: 'Takk!', was_this_helpful_comment: 'Noe du vil legge til?', + was_this_helpful_positive_label: 'Nyttig', + was_this_helpful_negative_label: 'Ikke nyttig', submit: 'Send inn', annotation_button_label: 'Åpne merknad', code_copied: 'Kopiert!', diff --git a/packages/gitbook/src/intl/translations/pt-br.ts b/packages/gitbook/src/intl/translations/pt-br.ts index 52f340c1b..b4f33d30c 100644 --- a/packages/gitbook/src/intl/translations/pt-br.ts +++ b/packages/gitbook/src/intl/translations/pt-br.ts @@ -32,6 +32,8 @@ export const pt_br = { was_this_helpful_negative: 'Não', was_this_helpful_thank_you: 'Obrigado!', was_this_helpful_comment: 'Gostaria de adicionar algo?', + was_this_helpful_positive_label: 'Útil', + was_this_helpful_negative_label: 'Não útil', submit: 'Enviar', annotation_button_label: 'Abrir anotação', code_copied: 'Copiado!', diff --git a/packages/gitbook/src/intl/translations/zh.ts b/packages/gitbook/src/intl/translations/zh.ts index 253468691..41dd7d1fb 100644 --- a/packages/gitbook/src/intl/translations/zh.ts +++ b/packages/gitbook/src/intl/translations/zh.ts @@ -33,6 +33,8 @@ export const zh: TranslationLanguage = { was_this_helpful_negative: '不', was_this_helpful_thank_you: '谢谢!', was_this_helpful_comment: '您有什么想补充的吗?', + was_this_helpful_positive_label: '有帮助', + was_this_helpful_negative_label: '没有帮助', submit: '提交', annotation_button_label: '打开批注', code_copied: '已复制!', diff --git a/packages/gitbook/src/lib/pages.test.ts b/packages/gitbook/src/lib/pages.test.ts index b1ab51e3c..f4cc66aca 100644 --- a/packages/gitbook/src/lib/pages.test.ts +++ b/packages/gitbook/src/lib/pages.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'bun:test'; -import type { RevisionPage } from '@gitbook/api'; +import { + type RevisionPage, + RevisionPageLayoutOptionsCoverSize, + RevisionPageLayoutOptionsWidth, +} from '@gitbook/api'; import { resolveFirstDocument, resolvePagePath, resolvePagePathDocumentOrGroup } from './pages'; @@ -28,11 +32,14 @@ describe('resolveFirstDocument', () => { pages: [], layout: { cover: true, + coverSize: RevisionPageLayoutOptionsCoverSize.Full, title: true, description: true, tableOfContents: true, outline: true, pagination: true, + width: RevisionPageLayoutOptionsWidth.Default, + metadata: true, }, }, ], @@ -75,11 +82,14 @@ describe('resolveFirstDocument', () => { pages: [], layout: { cover: true, + coverSize: RevisionPageLayoutOptionsCoverSize.Full, title: true, description: true, tableOfContents: true, outline: true, pagination: true, + width: RevisionPageLayoutOptionsWidth.Default, + metadata: true, }, }, ]; @@ -115,11 +125,14 @@ describe('resolvePagePath', () => { pages: [], layout: { cover: true, + coverSize: RevisionPageLayoutOptionsCoverSize.Full, title: true, description: true, tableOfContents: true, outline: true, pagination: true, + width: RevisionPageLayoutOptionsWidth.Default, + metadata: true, }, }, ]; @@ -185,11 +198,14 @@ describe('resolvePagePath', () => { pages: [], layout: { cover: true, + coverSize: RevisionPageLayoutOptionsCoverSize.Full, title: true, description: true, tableOfContents: true, outline: true, pagination: true, + width: RevisionPageLayoutOptionsWidth.Default, + metadata: true, }, }, ], diff --git a/packages/gitbook/src/lib/utils.ts b/packages/gitbook/src/lib/utils.ts index 25e88b4da..152651c12 100644 --- a/packages/gitbook/src/lib/utils.ts +++ b/packages/gitbook/src/lib/utils.ts @@ -49,10 +49,6 @@ export function defaultCustomization(): api.SiteCustomizationSettings { feedback: { enabled: false, }, - // TODO: remove aiSearch once the cache has been fully updated (after 11/07/2025) - aiSearch: { - enabled: true, - }, ai: { mode: api.CustomizationAIMode.None, }, @@ -68,6 +64,10 @@ export function defaultCustomization(): api.SiteCustomizationSettings { pagination: { enabled: true, }, + pageActions: { + externalAI: true, + markdown: true, + }, trademark: { enabled: true, },