Second pass

This commit is contained in:
Zeno Kapitein
2025-05-06 20:35:26 +02:00
parent fbf6951c71
commit fb9c8f4d2c
7 changed files with 103 additions and 128 deletions
@@ -1,15 +1,16 @@
'use client';
import { useEffect, useState } from 'react';
import { useEffect, useRef, 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 { toggle, setLoading, setToggle } = useAdaptiveContext();
const currentPage = usePageContext();
const visitedPages = useVisitedPages((state) => state.pages);
const visitedPagesRef = useRef(visitedPages);
const [summary, setSummary] = useState<{
pageSummary?: string;
@@ -17,7 +18,15 @@ export function AIPageSummary() {
}>({});
useEffect(() => {
if (!visitedPages?.length) return;
// Skip if the visited pages haven't changed
if (JSON.stringify(visitedPagesRef.current) === JSON.stringify(visitedPages)) return;
visitedPagesRef.current = visitedPages;
let canceled = false;
setLoading(true);
(async () => {
const stream = await streamPageSummary({
@@ -36,28 +45,51 @@ export function AIPageSummary() {
setSummary(summary);
}
})();
})().finally(() => {
setLoading(false);
if (!toggle.manual) {
setToggle({
open: true,
manual: false,
});
}
});
return () => {
canceled = true;
};
}, [currentPage, visitedPages]);
const shimmerBlocks = [20, 35, 25, 10, 45, 30, 30, 35, 25, 10, 40, 30]; // Widths in percentages
return (
open && (
toggle.open && (
<div className="flex animate-fadeIn flex-col gap-4">
{summary.pageSummary ? (
<div>
<h5 className="mb-1 font-semibold text-tint-subtle text-xs uppercase">
<h5 className="mb-0.5 font-semibold text-tint-subtle text-xs uppercase">
Key facts
</h5>
{summary.pageSummary}
</div>
) : null}
) : (
<div className="flex w-full flex-wrap gap-2">
{shimmerBlocks.map((width, index) => (
<div
key={index}
className="h-4 animate-pulse rounded bg-tint-active"
style={{
width: `${width}%`,
animationDelay: `${index * 0.1}s`,
}}
/>
))}
</div>
)}
{visitedPages.length > 1 && summary?.bigPicture ? (
<div>
<h5 className="mb-1 font-semibold text-tint-subtle text-xs uppercase">
<h5 className="mb-0.5 font-semibold text-tint-subtle text-xs uppercase">
Big Picture
</h5>
{summary?.bigPicture}
@@ -1,88 +1,31 @@
'use client';
import React, { useEffect } from 'react';
import { useVisitedPages } from '../Insights';
import { usePageContext } from '../PageContext';
export type SuggestedPage = {
id: string;
title: string;
href: string;
icon?: string;
emoji?: string;
};
export type Journey = {
label: string;
icon?: string;
pages?: Array<SuggestedPage>;
};
import React from 'react';
type AdaptiveContextType = {
journeys: Journey[];
selectedJourney: Journey | undefined;
setSelectedJourney: (journey: Journey | undefined) => void;
loading: boolean;
open: boolean;
setOpen: (open: boolean) => void;
setLoading: (loading: boolean) => void;
toggle: {
open: boolean;
manual: boolean;
};
setToggle: (toggle: { open: boolean; manual: boolean }) => void;
};
export const AdaptiveContext = React.createContext<AdaptiveContextType | null>(null);
export const JOURNEY_COUNT = 4;
/**
* Client side context provider to pass information about the current page.
*/
export function JourneyContextProvider({
children,
spaces,
}: { children: React.ReactNode; spaces: { id: string; title: string }[] }) {
const [journeys, setJourneys] = React.useState<Journey[]>([]);
const [selectedJourney, setSelectedJourney] = React.useState<Journey | undefined>(undefined);
export function AdaptiveContextProvider({ children }: { children: React.ReactNode }) {
const [loading, setLoading] = React.useState(true);
const [open, setOpen] = React.useState(true);
const currentPage = usePageContext();
const visitedPages = useVisitedPages((state) => state.pages);
useEffect(() => {
let canceled = false;
// setJourneys([]);
(async () => {
// 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;
// setJourneys((prev) => [...prev, journey]);
// }
setLoading(false);
})();
return () => {
canceled = true;
};
}, [currentPage.pageId, currentPage.spaceId, currentPage.title, visitedPages, spaces]);
const [toggle, setToggle] = React.useState({
open: false,
manual: false,
});
return (
<AdaptiveContext.Provider
value={{ journeys, selectedJourney, setSelectedJourney, loading, open, setOpen }}
>
<AdaptiveContext.Provider value={{ loading, setLoading, toggle, setToggle }}>
{children}
</AdaptiveContext.Provider>
);
@@ -5,13 +5,13 @@ import { AIPageSummary } from './AIPageSummary';
import { useAdaptiveContext } from './AdaptiveContext';
import { AdaptivePaneHeader } from './AdaptivePaneHeader';
export function AdaptivePane() {
const { open } = useAdaptiveContext();
const { toggle } = useAdaptiveContext();
return (
<div
className={tcls(
'flex flex-col gap-4 rounded-md straight-corners:rounded-none bg-tint-subtle ring-1 ring-tint-subtle ring-inset transition-all duration-300',
open ? 'w-72 px-4 py-4' : 'w-56 px-4 py-3'
toggle.open ? 'w-72 px-4 py-4' : 'w-56 px-4 py-3'
)}
>
<AdaptivePaneHeader />
@@ -6,7 +6,7 @@ import { Button, Loading } from '../primitives';
import { useAdaptiveContext } from './AdaptiveContext';
export function AdaptivePaneHeader() {
const { loading, open, setOpen } = useAdaptiveContext();
const { loading, toggle, setToggle } = useAdaptiveContext();
return (
<div className="flex flex-row items-center gap-3 rounded-md straight-corners:rounded-none transition-all duration-500">
@@ -29,11 +29,16 @@ export function AdaptivePaneHeader() {
</div>
<Button
variant="blank"
className={tcls('px-2 *:transition-transform', !open && '*:-rotate-45')}
className={tcls('px-2 *:transition-transform', !toggle.open && '*:-rotate-45')}
iconOnly
label="Close"
icon="close"
onClick={() => setOpen(!open)}
onClick={() =>
setToggle({
open: !toggle.open,
manual: true,
})
}
/>
</div>
);
@@ -141,28 +141,23 @@ export async function* streamPageSummary({
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
- Synthesize specific concepts from across multiple pages into concrete insights
- Highlight practical patterns and workflows that emerge when combining these concepts
- Focus on real capabilities that come from understanding multiple features together
- Use specific examples that show the value of combining these ideas
- Keep the language simple and direct
- Use a conversational tone and short sentences, without commas.
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."
✗ "GitBook combines content creation, collaboration, and integrations, building on your understanding of identifiers and paginated results for seamless documentation management."
✗ "The platform's robust features for content organization, versioning, and access control work together to create a powerful documentation ecosystem."
✗ "By leveraging GitBook's content blocks, permissions system, and API capabilities, you can build comprehensive documentation solutions."
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."`,
✓ "Combining Markdown tables with webhook notifications means your API docs stay up-to-date automatically - when you update a parameter, the PDF version refreshes too."
✓ "Content blocks and version history together solve the biggest docs headache - you can experiment with different layouts while keeping a clean record of what changed and why."
✓ "The real power comes from linking custom domains with content permissions - your sales team gets branded docs while your developers see the technical details on the same site."
✓ "With spaces, webhooks, and custom metadata working together, you're not just making docs - you're building a knowledge system that responds to how your team actually works."`,
},
{
role: AIMessageRole.Developer,
@@ -34,6 +34,7 @@ import { ClientContexts } from './ClientContexts';
import '@gitbook/icons/style.css';
import './globals.css';
import { GITBOOK_FONTS_URL, GITBOOK_ICONS_TOKEN, GITBOOK_ICONS_URL } from '@v2/lib/env';
import { AdaptiveContextProvider } from '../Adaptive/AdaptiveContext';
import { AnnouncementDismissedScript } from '../Announcement';
/**
@@ -175,7 +176,9 @@ export async function CustomizationRootLayout(props: {
: null) || IconStyle.Regular
}
>
<ClientContexts language={language}>{children}</ClientContexts>
<ClientContexts language={language}>
<AdaptiveContextProvider>{children}</AdaptiveContextProvider>
</ClientContexts>
</IconsProvider>
</body>
</html>
@@ -15,7 +15,6 @@ import { getPagePath } from '@/lib/pages';
import { isPageIndexable, isSiteIndexable } from '@/lib/seo';
import { getResizedImageURL } from '@v2/lib/images';
import { JourneyContextProvider } from '../Adaptive/AdaptiveContext';
import { PageContextProvider } from '../PageContext';
import { PageClientLayout } from './PageClientLayout';
import { type PagePathParams, fetchPageData, getPathnameParam } from './fetch';
@@ -71,32 +70,30 @@ export async function SitePage(props: SitePageProps) {
return (
<PageContextProvider pageId={page.id} spaceId={context.space.id} title={page.title}>
<JourneyContextProvider spaces={getSpaces(context.structure)}>
{withFullPageCover && page.cover ? (
<PageCover as="full" page={page} cover={page.cover} context={context} />
) : null}
{/* We use a flex row reverse to render the aside first because the page is streamed. */}
<div className="flex grow flex-row-reverse justify-end">
<PageAside
page={page}
document={document}
withHeaderOffset={headerOffset}
withFullPageCover={withFullPageCover}
withPageFeedback={withPageFeedback}
context={context}
/>
<PageBody
context={context}
page={page}
ancestors={ancestors}
document={document}
withPageFeedback={withPageFeedback}
/>
</div>
<React.Suspense fallback={null}>
<PageClientLayout withSections={withSections} />
</React.Suspense>
</JourneyContextProvider>
{withFullPageCover && page.cover ? (
<PageCover as="full" page={page} cover={page.cover} context={context} />
) : null}
{/* We use a flex row reverse to render the aside first because the page is streamed. */}
<div className="flex grow flex-row-reverse justify-end">
<PageAside
page={page}
document={document}
withHeaderOffset={headerOffset}
withFullPageCover={withFullPageCover}
withPageFeedback={withPageFeedback}
context={context}
/>
<PageBody
context={context}
page={page}
ancestors={ancestors}
document={document}
withPageFeedback={withPageFeedback}
/>
</div>
<React.Suspense fallback={null}>
<PageClientLayout withSections={withSections} />
</React.Suspense>
</PageContextProvider>
);
}