Merge remote-tracking branch 'origin/main' into stevenh/bump-shiki

This commit is contained in:
Steven Hall
2025-07-18 18:22:48 +01:00
24 changed files with 199 additions and 38 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
AI response feedback buttons
+2 -2
View File
@@ -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"],
+1 -1
View File
@@ -34,7 +34,7 @@
"workspaces": {
"packages": ["packages/*"],
"catalog": {
"@gitbook/api": "^0.128.0"
"@gitbook/api": "^0.129.0"
}
},
"patchedDependencies": {
+8 -4
View File
@@ -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;
+4 -4
View File
@@ -318,10 +318,6 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
feedback: {
enabled: false,
},
// TODO: remove aiSearch once the cache has been fully updated (after 11/07/2025)
aiSearch: {
enabled: true,
},
ai: {
mode: CustomizationAIMode.None,
},
@@ -337,6 +333,10 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
pagination: {
enabled: true,
},
pageActions: {
externalAI: true,
markdown: true,
},
trademark: {
enabled: true,
},
@@ -11,6 +11,7 @@ import { useAIMessageContextRef } from './useAIMessageContext';
export type AIChatMessage = {
role: AIMessageRole;
content: React.ReactNode;
query?: string;
};
export type AIChatState = {
@@ -24,6 +25,11 @@ export type AIChatState = {
*/
responseId: string | null;
/**
* The latest query sent to the AI.
*/
query: string | null;
/**
* Messages in the session.
*/
@@ -73,6 +79,7 @@ const globalState = zustand.create<{
opened: false,
responseId: null,
messages: [],
query: null,
followUpSuggestions: [],
loading: false,
error: false,
@@ -106,6 +113,7 @@ export function useAIChatController(): AIChatController {
opened: state.opened,
loading: false,
messages: [],
query: null,
followUpSuggestions: [],
responseId: null,
error: false,
@@ -128,6 +136,7 @@ export function useAIChatController(): AIChatController {
content: null,
},
],
query: input.message,
followUpSuggestions: [],
loading: true,
error: false,
@@ -2,6 +2,7 @@ import { tcls } from '@/lib/tailwind';
import { AIMessageRole } from '@gitbook/api';
import type React from 'react';
import type { AIChatController, AIChatState } from '../AI/useAIChat';
import { AIResponseFeedback } from './AIResponseFeedback';
import { AIChatFollowupSuggestions } from './AiChatFollowupSuggestions';
export function AIChatMessages(props: {
@@ -57,10 +58,19 @@ export function AIChatMessages(props: {
) : null}
{isLastMessage ? (
<AIChatFollowupSuggestions
chat={chat}
chatController={chatController}
/>
<>
{!chat.loading && !chat.error && chat.query && chat.responseId && (
<AIResponseFeedback
responseId={chat.responseId}
query={chat.query}
className="-ml-1 -mt-4"
/>
)}
<AIChatFollowupSuggestions
chat={chat}
chatController={chatController}
/>
</>
) : null}
</div>
);
@@ -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 (
<div className={tcls('flex h-fit items-center', className)}>
<Button
icon="thumbs-up"
iconOnly
label={tString(language, 'was_this_helpful_positive_label')}
variant="blank"
className={tcls(
'animate-fadeIn overflow-hidden text-tint-subtle transition-all',
rating !== null && rating !== 1 && 'px-0 text-[0] opacity-0'
)}
size="medium"
style={{ animationDuration: '.5s' }}
onClick={() => handleRating(1)}
disabled={rating !== null}
active={rating === 1}
key="positive"
/>
<Button
icon="thumbs-down"
iconOnly
label={tString(language, 'was_this_helpful_negative_label')}
variant="blank"
className={tcls(
'animate-fadeIn overflow-hidden text-tint-subtle transition-all',
rating !== null && rating !== -1 && 'px-0 text-[0] opacity-0'
)}
size="medium"
style={{ animationDelay: '.2s', animationDuration: '.5s' }}
onClick={() => handleRating(-1)}
disabled={rating !== null}
active={rating === -1}
key="negative"
/>
{rating !== null ? (
<span
className="ml-2 animate-fadeIn text-tint-subtle"
style={{ animationDelay: '.3s', animationDuration: '.5s' }}
>
{t(language, 'was_this_helpful_thank_you')}
</span>
) : null}
</div>
);
}
@@ -1,3 +1,4 @@
export * from './AIChat';
export * from './AIChatButton';
export * from './AIChatIcon';
export * from './AIResponseFeedback';
@@ -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 }) {
<div className="flex min-h-full p-4">
{askState?.type === 'answer' ? (
<React.Suspense fallback={loading}>
<TransitionAnswerBody answer={askState.answer} placeholder={loading} />
<TransitionAnswerBody
answer={askState.answer}
placeholder={loading}
query={query}
/>
</React.Suspense>
) : 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<AskAnswerResult | null>(null);
const [_isPending, startTransition] = React.useTransition();
@@ -127,21 +136,25 @@ function TransitionAnswerBody(props: { answer: AskAnswerResult; placeholder: Rea
return display ? (
<div className={tcls('w-full')}>
<AnswerBody answer={display} />
<AnswerBody query={query} answer={display} />
</div>
) : (
<>{placeholder}</>
);
}
function AnswerBody(props: { answer: AskAnswerResult }) {
const { answer } = props;
function AnswerBody(props: { query: string; answer: AskAnswerResult }) {
const { query, answer } = props;
const language = useLanguage();
return (
<>
<div data-testid="search-ask-answer" className="text-tint-strong">
{answer.body ?? t(language, 'search_ask_no_answer')}
{answer.sources.length > 0 ? (
// @TODO: Add responseId once search uses new AI endpoint
<AIResponseFeedback query={query} className="-ml-1 mt-2" responseId="" />
) : null}
{answer.followupQuestions.length > 0 ? (
<AnswerFollowupQuestions followupQuestions={answer.followupQuestions} />
) : null}
@@ -162,7 +175,7 @@ function AnswerFollowupQuestions(props: { followupQuestions: string[] }) {
const getSearchLinkProps = useSearchLink();
return (
<div className={tcls('flex', 'flex-col', 'flex-wrap', 'mt-4', 'sm:mt-6')}>
<div className={tcls('flex', 'flex-col', 'flex-wrap', 'mt-4')}>
{followupQuestions.map((question) => (
<Link
key={question}
@@ -31,6 +31,8 @@ export const variantClasses = {
'hover:text-contrast-primary-solid-hover',
'border-0',
'contrast-more:border-1',
'disabled:bg-tint',
'disabled:text-tint/8',
],
blank: [
'bg-transparent',
@@ -42,6 +44,8 @@ export const variantClasses = {
'hover:scale-1',
'contrast-more:bg-tint-subtle',
'depth-subtle:hover:translate-y-0',
'disabled:text-tint/8',
'disabled:bg-transparent',
],
secondary: [
'bg-tint',
@@ -51,6 +55,8 @@ export const variantClasses = {
'depth-flat:hover:bg-tint-hover',
'hover:text-primary',
'contrast-more:bg-tint-subtle',
'disabled:bg-transparent',
'disabled:text-tint/8',
],
header: [
'bg-tint-base text-tint',
@@ -75,7 +81,7 @@ export const variantClasses = {
const activeClasses = {
primary: 'bg-primary-solid-hover',
blank: 'bg-primary-active text-primary-strong font-medium hover:text-primary-strong hover:bg-primary-active',
blank: 'bg-primary-active disabled:bg-primary-active text-primary-strong font-medium hover:text-primary-strong disabled:text-primary-strong hover:bg-primary-active',
secondary: 'bg-tint-active',
header: 'bg-header-link/3',
};
@@ -99,13 +105,14 @@ export const Button = React.forwardRef<
children,
active,
trailing,
disabled,
...rest
},
ref
) => {
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}
</button>
);
return iconOnly && label ? <Tooltip label={label}>{button}</Tooltip> : button;
return iconOnly && label ? (
<Tooltip
rootProps={{ open: disabled === true ? false : undefined }}
label={label}
triggerProps={{ disabled }}
>
{button}
</Tooltip>
) : (
button
);
}
);
@@ -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 = [
@@ -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;
@@ -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!',
@@ -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!',
@@ -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!',
@@ -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é !',
@@ -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: 'コピーしました!',
@@ -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!',
@@ -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!',
@@ -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!',
@@ -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: '已复制!',
+17 -1
View File
@@ -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,
},
},
],
+4 -4
View File
@@ -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,
},