Remove AI page link summaries (#3418)

This commit is contained in:
Samy Pessé
2025-07-02 08:40:55 +02:00
committed by GitHub
parent 28008667ed
commit 57f951a7d9
16 changed files with 2 additions and 414 deletions
@@ -1,190 +0,0 @@
'use client';
import { useLanguage } from '@/intl/client';
import { t } from '@/intl/translate';
import { Icon } from '@gitbook/icons';
import { useEffect } from 'react';
import { create } from 'zustand';
import { useShallow } from 'zustand/react/shallow';
import { usePageContext } from '../PageContext';
import { useVisitedPages } from '../hooks';
import { Loading } from '../primitives';
import { streamLinkPageSummary } from './server-actions/streamLinkPageSummary';
/**
* Get a unique cache key for a page summary
*/
function getCacheKey(targetSpaceId: string, targetPageId: string): string {
return `${targetSpaceId}:${targetPageId}`;
}
/**
* Global state for the summaries.
*/
const useSummaries = create<{
/**
* Cache of all summaries generated so far.
*/
cache: Map<string, string>;
/**
* Get a summary for a page.
*/
getSummary: (params: { targetSpaceId: string; targetPageId: string }) => string;
/**
* Stream the generation of a summary for a page.
*/
streamSummary: (params: {
currentSpaceId: string;
currentPageId: string;
currentPageTitle: string;
targetSpaceId: string;
targetPageId: string;
linkPreview?: string;
linkTitle?: string;
visitedPages: { spaceId: string; pageId: string }[];
}) => Promise<void>;
}>((set, get) => ({
cache: new Map(),
getSummary: ({
targetSpaceId,
targetPageId,
}: {
targetSpaceId: string;
targetPageId: string;
}) => {
return get().cache.get(getCacheKey(targetSpaceId, targetPageId)) ?? '';
},
streamSummary: async ({
currentSpaceId,
currentPageId,
currentPageTitle,
targetSpaceId,
targetPageId,
linkPreview,
linkTitle,
visitedPages,
}) => {
const cacheKey = getCacheKey(targetSpaceId, targetPageId);
if (get().cache.has(cacheKey)) {
// Already generated or generating
return;
}
const update = (summary: string) => {
set((prev) => {
const newCache = new Map(prev.cache);
newCache.set(cacheKey, summary);
return { cache: newCache };
});
};
update('');
const stream = await streamLinkPageSummary({
currentSpaceId,
currentPageId,
currentPageTitle,
targetSpaceId,
targetPageId,
linkPreview,
linkTitle,
visitedPages,
});
let generatedSummary = '';
for await (const highlight of stream) {
generatedSummary = highlight ?? '';
update(generatedSummary);
}
},
}));
/**
* Summarise a page's content for use in a link preview
*/
export function AIPageLinkSummary(props: {
targetSpaceId: string;
targetPageId: string;
linkPreview?: string;
linkTitle?: string;
showTrademark: boolean;
}) {
const { targetSpaceId, targetPageId, linkPreview, linkTitle, showTrademark = true } = props;
const currentPage = usePageContext();
const language = useLanguage();
const visitedPages = useVisitedPages();
const { summary, streamSummary } = useSummaries(
useShallow((state) => {
return {
summary: state.getSummary({ targetSpaceId, targetPageId }),
streamSummary: state.streamSummary,
};
})
);
useEffect(() => {
streamSummary({
currentSpaceId: currentPage.spaceId,
currentPageId: currentPage.pageId,
currentPageTitle: currentPage.title,
targetSpaceId,
targetPageId,
linkPreview,
linkTitle,
visitedPages,
});
}, [
currentPage.pageId,
currentPage.spaceId,
currentPage.title,
targetSpaceId,
targetPageId,
linkPreview,
linkTitle,
visitedPages,
streamSummary,
]);
const shimmerBlocks = [
'w-[20%] [animation-delay:-1s]',
'w-[35%] [animation-delay:-0.8s]',
'w-[25%] [animation-delay:-0.6s]',
'w-[10%] [animation-delay:-0.4s]',
'w-[40%] [animation-delay:-0.2s]',
'w-[30%] [animation-delay:0s]',
];
return (
<div className="flex flex-col gap-1">
<div className="flex w-screen items-center gap-1 font-semibold text-tint text-xs uppercase leading-tight tracking-wide">
{showTrademark ? (
<Loading className="size-4" busy={!summary || summary.length === 0} />
) : (
<Icon icon="sparkle" className="size-3" />
)}
<h6 className="text-tint">{t(language, 'link_tooltip_ai_summary')}</h6>
</div>
{summary.length > 0 ? (
<p className="animate-fadeIn">{summary}</p>
) : (
<div className="mt-2 flex flex-wrap gap-2">
{shimmerBlocks.map((block, index) => (
<div
key={`${index}-${block}`}
className={`${block} h-4 animate-pulse rounded straight-corners:rounded-none bg-tint-active`}
/>
))}
</div>
)}
{summary.length > 0 ? (
<div className="animate-fadeIn text-tint-subtle text-xs">
{t(language, 'link_tooltip_ai_summary_description')}
</div>
) : null}
</div>
);
}
@@ -1 +0,0 @@
export * from './AIPageLinkSummary';
@@ -1 +0,0 @@
export * from './streamLinkPageSummary';
@@ -1,157 +0,0 @@
'use server';
import { getSiteURLDataFromMiddleware } from '@/lib/middleware';
import { getServerActionBaseContext } from '@/lib/server-actions';
import { filterOutNullable } from '@/lib/typescript';
import { AIMessageRole, AIModel } from '@gitbook/api';
import { z } from 'zod';
import { streamGenerateAIObject } from '../../AI/server-actions/api';
/**
* Get a summary of a page, in the context of another page
*/
export async function* streamLinkPageSummary({
currentSpaceId,
currentPageId,
targetSpaceId,
targetPageId,
linkPreview,
linkTitle,
visitedPages,
}: {
currentSpaceId: string;
currentPageId: string;
currentPageTitle: string;
targetSpaceId: string;
targetPageId: string;
linkPreview?: string;
linkTitle?: string;
visitedPages?: Array<{ spaceId: string; pageId: string }>;
}) {
const baseContext = await getServerActionBaseContext();
const siteURLData = await getSiteURLDataFromMiddleware();
const { stream } = await streamGenerateAIObject(baseContext, {
organizationId: siteURLData.organization,
siteId: siteURLData.site,
model: AIModel.Fast,
schema: z.object({
highlight: z.string().describe('The reason why the user should read the target page.'),
}),
input: [
{
role: AIMessageRole.Developer,
content: `# 1. Role
You are a contextual fact extractor. Your job is to find the exact fact from the linked page that directly answers the implied question in the current paragraph.
# 2. Task
Extract a contextually-relevant fact that:
- Directly answers the specific need or question implied by the link's placement
- States a capability, limitation, or specification from the target page
- Connects precisely to the user's current paragraph or sentence
- Completes the user's understanding based on what they're currently reading
# 3. Instructions
1. First, identify the exact need, question, or gap in the current paragraph where the link appears
2. Find the specific fact in the target page that addresses this exact contextual need
3. Ensure the fact relates directly to the context of the paragraph containing the link
4. Avoid ALL instructional language including words like "use", "click", "select", "create"
5. Keep it under 30 words, factual and declarative about what EXISTS or IS TRUE`,
},
{
role: AIMessageRole.Developer,
content: `# 4. Current page
The content of the current page is:`,
attachments: [
{
type: 'page' as const,
spaceId: currentSpaceId,
pageId: currentPageId,
},
],
},
...(visitedPages
? [
{
role: AIMessageRole.Developer,
content: '# 5. Previous pages',
},
...visitedPages.map(({ spaceId, pageId }) => ({
role: AIMessageRole.Developer,
content: `## Page ${pageId}`,
attachments: [
{
type: 'page' as const,
spaceId,
pageId,
},
],
})),
]
: []),
{
role: AIMessageRole.Developer,
content: `# 6. Target page
The content of the target page is:`,
attachments: [
{
type: 'page' as const,
spaceId: targetSpaceId,
pageId: targetPageId,
},
],
},
{
role: AIMessageRole.Developer,
content: `# 7. Link preview
The content of the link preview is:
> ${linkPreview}
> Page ID: ${targetPageId}`,
},
{
role: AIMessageRole.Developer,
content: `# 8. Guidelines & Examples
ALWAYS:
- ALWAYS choose facts that directly fulfill the contextual need where the link appears
- ALWAYS connect target page information specifically to the current paragraph context
- ALWAYS focus on the gap in knowledge that the link is meant to fill
- ALWAYS consider user's navigation history to ensure contextual continuity
- ALWAYS use action verbs like "click", "select", "use", "create", "enable"
NEVER:
- NEVER include ANY unspecifc language like "learn", "how to", "discover", etc. State the fact directly.
- NEVER select general facts unrelated to the specific link context
- NEVER ignore the specific context where the link appears
- NEVER repeat the same fact in different words
## Examples
Current paragraph: "When organizing content, headings are limited to 3 levels. For more advanced editing, you can use (multiple select)[/multiple-select] to move multiple blocks at once."
Preview: "Multiple Select: Select multiple content blocks at once."
✓ "Shift selects content between two points, useful for reorganizing your current heading structure."
✗ "Shift and Ctrl/Cmd keys are the modifiers for selecting multiple blocks."
Current paragraph: "Most changes can be published directly, but for major revisions, if you want others to review changes before publishing, create a (change request)[/change-requests]."
Preview: "Change Requests: Collaborative content editing workflow."
✓ "Each reviewer's approval is tracked separately, with specific change highlighting for your major revisions."
✗ "Each reviewer receives an email notification and can approve or request changes."
Current paragraph: "Your team mentioned issues with conflicting edits. Need to collaborate in real-time? You can use (live edit mode)[/live-edit]."
Preview: "Live Edit: Real-time collaborative editing."
✓ "Teams with GitHub repositories (like yours) cannot use this feature due to sync limitations."
✗ "Incompatible with GitHub/GitLab sync and requires specific visibility settings."`,
},
{
role: AIMessageRole.User,
content: `I'm considering reading the link titled "${linkTitle}" pointing to page ${targetPageId}. Why should I read it? Relate it to the paragraph I'm currently reading.`,
},
].filter(filterOutNullable),
});
for await (const value of stream) {
const highlight = value.highlight;
if (!highlight) {
continue;
}
yield highlight;
}
}
@@ -108,37 +108,11 @@ function InlineLinkTooltipWrapper(props: {
resolved.subText = undefined;
}
const aiSummary: { pageId: string; spaceId: string } | undefined = (() => {
if (isExternal) {
return;
}
if (isSamePage) {
return;
}
if (!('customization' in context) || !context.customization.ai?.pageLinkSummaries.enabled) {
return;
}
if (!('page' in context) || !('page' in inline.data.ref)) {
return;
}
if (inline.data.ref.kind === 'page' || inline.data.ref.kind === 'anchor') {
return {
pageId: resolved.page?.id ?? inline.data.ref.page ?? context.page.id,
spaceId: inline.data.ref.space ?? context.space.id,
};
}
})();
return (
<InlineLinkTooltip
breadcrumbs={breadcrumbs}
isExternal={isExternal}
isSamePage={isSamePage}
aiSummary={aiSummary}
openInNewTabLabel={tString(language, 'open_in_new_tab')}
target={{
href: resolved.href,
@@ -35,7 +35,6 @@ const InlineLinkTooltipImpl = dynamic(
export function InlineLinkTooltip(props: {
isSamePage: boolean;
isExternal: boolean;
aiSummary?: { pageId: string; spaceId: string };
breadcrumbs: Array<{ href?: string; label: string; icon?: React.ReactNode }>;
target: {
href: string;
@@ -3,13 +3,11 @@ import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons';
import * as Tooltip from '@radix-ui/react-tooltip';
import { Fragment } from 'react';
import { AIPageLinkSummary } from '../../AIPageLinkSummary';
import { Button, StyledLink } from '../../primitives';
export function InlineLinkTooltipImpl(props: {
isSamePage: boolean;
isExternal: boolean;
aiSummary?: { pageId: string; spaceId: string };
breadcrumbs: Array<{ href?: string; label: string; icon?: React.ReactNode }>;
target: {
href: string;
@@ -20,8 +18,7 @@ export function InlineLinkTooltipImpl(props: {
openInNewTabLabel: string;
children: React.ReactNode;
}) {
const { isSamePage, isExternal, aiSummary, openInNewTabLabel, target, breadcrumbs, children } =
props;
const { isSamePage, isExternal, openInNewTabLabel, target, breadcrumbs, children } = props;
return (
<Tooltip.Provider delayDuration={200}>
@@ -102,22 +99,8 @@ export function InlineLinkTooltipImpl(props: {
<p className="mt-1 text-sm text-tint">{target.subText}</p>
) : null}
</div>
{aiSummary ? (
<div className="border-tint-subtle border-t bg-tint p-4">
<AIPageLinkSummary
targetPageId={aiSummary.pageId}
targetSpaceId={aiSummary.spaceId}
showTrademark
/>
</div>
) : null}
</div>
<Tooltip.Arrow
className={
typeof aiSummary !== 'undefined' ? 'fill-tint-3' : 'fill-tint-1'
}
/>
<Tooltip.Arrow className="fill-tint-1" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
@@ -63,8 +63,6 @@ export const de = {
more: 'Mehr',
link_tooltip_external_link: 'Externe Verlinkung zu',
link_tooltip_page_anchor: 'Zum Abschnitt springen',
link_tooltip_ai_summary: 'Seitenhighlight',
link_tooltip_ai_summary_description: 'Basierend auf Ihrem Kontext. Kann Fehler enthalten.',
open_in_new_tab: 'In neuem Tab öffnen',
ai_chat_assistant_name: 'Docs-Assistent',
ai_chat_assistant_description: 'Ich helfe Ihnen bei der Dokumentation.',
@@ -61,8 +61,6 @@ export const en = {
more: 'More',
link_tooltip_external_link: 'External link to',
link_tooltip_page_anchor: 'Jump to section',
link_tooltip_ai_summary: 'Page highlight',
link_tooltip_ai_summary_description: 'Based on your context. May contain mistakes.',
open_in_new_tab: 'Open in new tab',
ai_chat_assistant_name: 'Docs Assistant',
ai_chat_assistant_description: "I'm here to help you with the docs.",
@@ -65,8 +65,6 @@ export const es: TranslationLanguage = {
more: 'Más',
link_tooltip_external_link: 'Enlace externo a',
link_tooltip_page_anchor: 'Saltar a la sección',
link_tooltip_ai_summary: 'Resumen de la página',
link_tooltip_ai_summary_description: 'Basado en tu contexto. Puede contener errores.',
open_in_new_tab: 'Abrir en una nueva pestaña',
ai_chat_assistant_name: 'Asistente de Docs',
ai_chat_assistant_description: 'Estoy aquí para ayudarte con la documentación.',
@@ -63,8 +63,6 @@ export const fr: TranslationLanguage = {
more: 'Plus',
link_tooltip_external_link: 'Lien externe à',
link_tooltip_page_anchor: 'Sauter à la section',
link_tooltip_ai_summary: 'Résumé de la page',
link_tooltip_ai_summary_description: 'Basé sur votre contexte. Peut contenir des erreurs.',
open_in_new_tab: 'Ouvrir dans un nouvel onglet',
ai_chat_assistant_name: 'Assistant Docs',
ai_chat_assistant_description: 'Je suis là pour vous aider avec la documentation.',
@@ -63,9 +63,6 @@ export const ja: TranslationLanguage = {
more: '詳細',
link_tooltip_external_link: '外部リンク先',
link_tooltip_page_anchor: 'ページ内リンク先',
link_tooltip_ai_summary: 'ページのハイライト',
link_tooltip_ai_summary_description:
'あなたのコンテキストに基づいています。間違いが含まれる可能性があります。',
open_in_new_tab: '新しいタブで開く',
ai_chat_assistant_name: 'ドキュメントアシスタント',
ai_chat_assistant_description: 'ドキュメントについてお手伝いします。',
@@ -63,8 +63,6 @@ export const nl: TranslationLanguage = {
more: 'Meer',
link_tooltip_external_link: 'Externe link naar',
link_tooltip_page_anchor: 'Spring naar sectie',
link_tooltip_ai_summary: 'Pagina-samenvatting',
link_tooltip_ai_summary_description: 'Gebaseerd op je context. Kan fouten bevatten.',
open_in_new_tab: 'Open in nieuw tabblad',
ai_chat_assistant_name: 'Docs Assistent',
ai_chat_assistant_description: 'Ik help je met de documentatie.',
@@ -63,8 +63,6 @@ export const no: TranslationLanguage = {
more: 'Mer',
link_tooltip_external_link: 'Ekstern lenke til',
link_tooltip_page_anchor: 'Hopp til seksjon',
link_tooltip_ai_summary: 'Sidesammendrag',
link_tooltip_ai_summary_description: 'Basert på din kontekst. Kan inneholde feil.',
open_in_new_tab: 'Åpne i ny fane',
ai_chat_assistant_name: 'Docs-assistent',
ai_chat_assistant_description: 'Jeg er her for å hjelpe deg med docs.',
@@ -63,8 +63,6 @@ export const pt_br = {
more: 'Mais',
link_tooltip_external_link: 'Link externo para',
link_tooltip_page_anchor: 'Pular para a seção',
link_tooltip_ai_summary: 'Resumo da página',
link_tooltip_ai_summary_description: 'Baseado no seu contexto. Pode conter erros.',
open_in_new_tab: 'Abrir em uma nova guia',
ai_chat_assistant_name: 'Assistente de Docs',
ai_chat_assistant_description: 'Estou aqui para ajudá-lo com a documentação.',
@@ -61,8 +61,6 @@ export const zh: TranslationLanguage = {
more: '更多',
link_tooltip_external_link: '外部链接到',
link_tooltip_page_anchor: '跳转到页面',
link_tooltip_ai_summary: '页面要点',
link_tooltip_ai_summary_description: '基于您的上下文。可能包含错误。',
open_in_new_tab: '在新标签页中打开',
ai_chat_assistant_name: '文档助手',
ai_chat_assistant_description: '我在这里帮助您了解文档。',