mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 01:53:26 +00:00
Remove AI page link summaries (#3418)
This commit is contained in:
@@ -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';
|
||||
-157
@@ -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: '我在这里帮助您了解文档。',
|
||||
|
||||
Reference in New Issue
Block a user