mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-18 00:25:07 +00:00
Second pass
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user