From fbf6951c71d2df97e3503b05e44b2626045b958f Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Tue, 6 May 2025 16:55:06 +0200 Subject: [PATCH] First pass --- .../src/components/Adaptive/AIPageSummary.tsx | 69 ++++++ .../components/Adaptive/AdaptiveContext.tsx | 35 ++- .../src/components/Adaptive/AdaptivePane.tsx | 8 +- .../server-actions/streamPageSummary.ts | 209 ++++++++++++++++++ .../src/components/PageAside/PageAside.tsx | 2 +- 5 files changed, 300 insertions(+), 23 deletions(-) create mode 100644 packages/gitbook/src/components/Adaptive/AIPageSummary.tsx create mode 100644 packages/gitbook/src/components/Adaptive/server-actions/streamPageSummary.ts diff --git a/packages/gitbook/src/components/Adaptive/AIPageSummary.tsx b/packages/gitbook/src/components/Adaptive/AIPageSummary.tsx new file mode 100644 index 000000000..8f0eae0eb --- /dev/null +++ b/packages/gitbook/src/components/Adaptive/AIPageSummary.tsx @@ -0,0 +1,69 @@ +'use client'; +import { useEffect, useState } from 'react'; +import { useVisitedPages } from '../Insights'; +import { usePageContext } from '../PageContext'; +import { useAdaptiveContext } from './AdaptiveContext'; +import { streamPageSummary } from './server-actions/streamPageSummary'; + +export function AIPageSummary() { + const { open } = useAdaptiveContext(); + + const currentPage = usePageContext(); + const visitedPages = useVisitedPages((state) => state.pages); + + const [summary, setSummary] = useState<{ + pageSummary?: string; + bigPicture?: string; + }>({}); + + useEffect(() => { + let canceled = false; + + (async () => { + const stream = await streamPageSummary({ + currentPage: { + id: currentPage.pageId, + title: currentPage.title, + }, + currentSpace: { + id: currentPage.spaceId, + }, + visitedPages: visitedPages, + }); + + for await (const summary of stream) { + if (canceled) return; + + setSummary(summary); + } + })(); + + return () => { + canceled = true; + }; + }, [currentPage, visitedPages]); + + return ( + open && ( +
+ {summary.pageSummary ? ( +
+
+ Key facts +
+ {summary.pageSummary} +
+ ) : null} + + {visitedPages.length > 1 && summary?.bigPicture ? ( +
+
+ Big Picture +
+ {summary?.bigPicture} +
+ ) : null} +
+ ) + ); +} diff --git a/packages/gitbook/src/components/Adaptive/AdaptiveContext.tsx b/packages/gitbook/src/components/Adaptive/AdaptiveContext.tsx index 002818937..22b883cb8 100644 --- a/packages/gitbook/src/components/Adaptive/AdaptiveContext.tsx +++ b/packages/gitbook/src/components/Adaptive/AdaptiveContext.tsx @@ -3,7 +3,6 @@ import React, { useEffect } from 'react'; import { useVisitedPages } from '../Insights'; import { usePageContext } from '../PageContext'; -import { streamPageJourneySuggestions } from './server-actions'; export type SuggestedPage = { id: string; @@ -50,27 +49,27 @@ export function JourneyContextProvider({ useEffect(() => { let canceled = false; - setJourneys([]); + // setJourneys([]); (async () => { - const stream = await streamPageJourneySuggestions({ - count: JOURNEY_COUNT, - currentPage: { - id: currentPage.pageId, - title: currentPage.title, - }, - currentSpace: { - id: currentPage.spaceId, - }, - allSpaces: spaces, - visitedPages, - }); + // const stream = await streamPageJourneySuggestions({ + // count: JOURNEY_COUNT, + // currentPage: { + // id: currentPage.pageId, + // title: currentPage.title, + // }, + // currentSpace: { + // id: currentPage.spaceId, + // }, + // allSpaces: spaces, + // visitedPages, + // }); - for await (const journey of stream) { - if (canceled) return; + // for await (const journey of stream) { + // if (canceled) return; - setJourneys((prev) => [...prev, journey]); - } + // setJourneys((prev) => [...prev, journey]); + // } setLoading(false); })(); diff --git a/packages/gitbook/src/components/Adaptive/AdaptivePane.tsx b/packages/gitbook/src/components/Adaptive/AdaptivePane.tsx index c56134c8a..628bb4a5a 100644 --- a/packages/gitbook/src/components/Adaptive/AdaptivePane.tsx +++ b/packages/gitbook/src/components/Adaptive/AdaptivePane.tsx @@ -1,8 +1,7 @@ 'use client'; import { tcls } from '@/lib/tailwind'; -import { AINextPageSuggestions } from './AINextPageSuggestions'; -import { AIPageJourneySuggestions } from './AIPageJourneySuggestions'; +import { AIPageSummary } from './AIPageSummary'; import { useAdaptiveContext } from './AdaptiveContext'; import { AdaptivePaneHeader } from './AdaptivePaneHeader'; export function AdaptivePane() { @@ -16,8 +15,9 @@ export function AdaptivePane() { )} > - - + + {/* + */} ); } diff --git a/packages/gitbook/src/components/Adaptive/server-actions/streamPageSummary.ts b/packages/gitbook/src/components/Adaptive/server-actions/streamPageSummary.ts new file mode 100644 index 000000000..bc9051cb4 --- /dev/null +++ b/packages/gitbook/src/components/Adaptive/server-actions/streamPageSummary.ts @@ -0,0 +1,209 @@ +'use server'; +import { getV1BaseContext } from '@/lib/v1'; +import { isV2 } from '@/lib/v2'; +import { AIMessageRole } from '@gitbook/api'; +import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware'; +import { fetchServerActionSiteContext, getServerActionBaseContext } from '@v2/lib/server-actions'; +import { z } from 'zod'; +import { streamGenerateObject } from './api'; + +/** + * Get a summary of a page, in the context of another page + */ +export async function* streamPageSummary({ + currentPage, + currentSpace, + visitedPages, +}: { + currentPage: { + id: string; + title: string; + }; + currentSpace: { + id: string; + // title: string; + }; + visitedPages: { + pageId: string; + spaceId: string; + }[]; +}) { + const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext(); + const siteURLData = await getSiteURLDataFromMiddleware(); + + const [{ stream }] = await Promise.all([ + streamGenerateObject( + baseContext, + { + organizationId: siteURLData.organization, + siteId: siteURLData.site, + }, + { + schema: z.object({ + pageSummary: z + .string() + .describe( + 'A collection of key facts from the page that together form a comprehensive summary. Keep it under 30 words.' + ), + bigPicture: + visitedPages.length > 0 + ? z + .string() + .describe( + 'A natural-sounding summary of how specific concepts connect with real benefits. Use a conversational tone with concrete examples. Avoid overly formal language while still being specific. Keep it under 30 words.' + ) + : z.undefined(), + }), + tools: { + // getPages: true, + // getPageContent: true, + }, + messages: [ + { + role: AIMessageRole.Developer, + content: `# 1. Role + You are a fact extractor. Your job is to identify and extract the most important facts from the current page. + + # 2. Task + Extract multiple key facts that: + - Cover the most important concepts, features, or capabilities on the page + - Represent specific, actionable information rather than general descriptions + - Provide concrete details about functionality, limitations, or specifications + - Together form a comprehensive understanding of the page content + - Relate to the user's learning journey through the documentation when relevant + + # 3. Instructions + 1. Analyze the current page to identify 3-5 concrete, specific facts (not general summaries) + 2. Focus on facts that would be most useful and relevant to someone using this documentation + 3. If the user has visited other pages, identify facts that build upon their previous knowledge + 4. Present facts as clear, declarative statements about what exists or is true + 5. Separate distinct facts rather than combining them into a single summary + 6. Include specific details, numbers, limitations, or capabilities where available`, + }, + { + role: AIMessageRole.Developer, + content: `# 4. Current page + The content of the current page is:`, + attachments: [ + { + type: 'page' as const, + spaceId: currentSpace.id, + pageId: currentPage.id, + }, + ], + }, + ...(visitedPages && visitedPages.length > 0 + ? [ + { + role: AIMessageRole.Developer, + content: `# 5. Previous Pages and Learning Journey + The content across ${visitedPages.length} page(s) builds a knowledge framework. Use this to: + - Identify specific, concrete ways concepts interact (not just "work together") + - Show exact functional relationships between ideas (not vague "enhances") + - Highlight tangible capabilities that emerge from combined concepts + - Describe precise benefits that result from these connections + - Focus on what becomes possible when these concepts are combined + + The content of up to 5 most recent pages are included below:`, + }, + ...visitedPages.slice(0, 5).map(({ spaceId, pageId }, index) => ({ + role: AIMessageRole.Developer, + content: `## Previous Page ${index + 1}: ${pageId}`, + attachments: [ + { + type: 'page' as const, + spaceId, + pageId, + }, + ], + })), + ] + : []), + { + role: AIMessageRole.Developer, + content: `# 6. Guidelines for Fact Extraction + ALWAYS: + - Extract multiple distinct facts rather than a single summary + - Focus on specific, concrete details rather than general descriptions + - Include numbers, limitations, requirements, or specifications when available + - Prioritize facts that would be most useful to someone using the documentation + - Consider how facts on this page relate to previously visited pages + + NEVER: + - Use instructional language like "learn", "how to", "discover", etc. + - Include vague or generic statements that lack specific details + - Repeat the page title without adding informative value + - Combine multiple distinct facts into a single general statement`, + }, + { + role: AIMessageRole.Developer, + content: `## Big Picture Guidelines + For the big picture summary: + + ALWAYS: + - Use a natural, conversational tone a person would actually use + - Include concrete examples with specific benefits + - Balance being precise with sounding natural + - Use occasional contractions or slightly informal phrasing + - Write as if explaining to a colleague in a friendly way + + NEVER: + - Use empty relationship words like "enhances," "supports," or "integrates with" + - Write in an overly academic or technical style + - Use abstract phrases without concrete meaning + - Sound like marketing copy or documentation + - Lose specificity while trying to sound conversational + + POOR EXAMPLES TO AVOID: + ✗ "Markdown enhances content creation by integrating with collaboration features." + ✗ "API components support the documentation workflow through seamless integration." + ✗ "The robust search functionality facilitates efficient information retrieval." + + GOOD EXAMPLES TO FOLLOW: + ✓ "Markdown tables make API data easier to read, while code blocks let you test examples right in the docs." + ✓ "Webhooks save tons of time by automatically creating PDFs whenever content changes." + ✓ "Version control pins down exactly who changed what text, so you won't waste time on formatting debates."`, + }, + { + role: AIMessageRole.Developer, + content: `## Examples + + Page content: "Content blocks in GitBook include text, images, videos, code snippets, and more. Each block can be customized with specific settings. Text blocks support Markdown formatting and can include inline code. Images can be resized and have alt text added." + ✓ "Text blocks support Markdown formatting. Images can be resized and include alt text. Available block types include text, images, videos, and code snippets." + ✗ "GitBook offers various content blocks with customization options." + + Page content: "Change Requests allow teams to propose, review, and approve content changes before publishing. Each reviewer's approval is tracked separately. Changes are highlighted with color coding. Change Requests can be merged automatically or manually after approval." + ✓ "Reviewer approvals are tracked individually. Changes are color-coded for visibility. Merging can be automatic or manual after approval. Multiple reviewers can collaborate on a single Change Request." + ✗ "Change Requests provide a collaborative workflow for content changes." + + Page content: "API authentication requires an API key generated in account settings. Keys expire after 90 days by default. Rate limits are set to 1000 requests per hour. Keys can have read-only or read-write permissions." + ✓ "API keys expire after 90 days by default. Rate limits are capped at 1000 requests per hour. Keys can be configured with read-only or read-write permissions." + ✗ "API keys are required for authentication and have various settings."`, + }, + { + role: AIMessageRole.Developer, + content: `The current page is: "${currentPage.title}" (ID ${currentPage.id})`, + }, + { + role: AIMessageRole.User, + content: + 'What are the key facts on this page, and what have I learned across the documentation so far?', + }, + ], + } + ), + fetchServerActionSiteContext(baseContext), + ]); + + for await (const value of stream) { + const pageSummary = value.pageSummary; + const bigPicture = value.bigPicture; + + if (!pageSummary) continue; + + yield { + pageSummary, + bigPicture, + }; + } +} diff --git a/packages/gitbook/src/components/PageAside/PageAside.tsx b/packages/gitbook/src/components/PageAside/PageAside.tsx index 99cb7a2a2..a5bf845dd 100644 --- a/packages/gitbook/src/components/PageAside/PageAside.tsx +++ b/packages/gitbook/src/components/PageAside/PageAside.tsx @@ -29,7 +29,7 @@ export function PageAside(props: { const { page, document, withPageFeedback, context } = props; const { customization, site, space } = context; - const useAdaptivePane = true; + const useAdaptivePane = customization.ai?.pageLinkSummaries.enabled; return (