mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-07 13:52:40 +00:00
Make link summaries work
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { streamLinkPageSummary } from './server-actions/streamLinkPageSummary';
|
||||
|
||||
/**
|
||||
* Summarise a page's content for use in a link preview
|
||||
*/
|
||||
export function AIPageLinkSummary(props: {
|
||||
currentSpaceId: string;
|
||||
currentPageId: string;
|
||||
targetSpaceId: string;
|
||||
targetPageId: string;
|
||||
}) {
|
||||
const { currentSpaceId, currentPageId, targetSpaceId, targetPageId } = props;
|
||||
|
||||
const [summary, setSummary] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
|
||||
setSummary('');
|
||||
|
||||
(async () => {
|
||||
const stream = await streamLinkPageSummary({
|
||||
currentSpaceId,
|
||||
currentPageId,
|
||||
targetSpaceId,
|
||||
targetPageId,
|
||||
previousPageIds: [],
|
||||
});
|
||||
|
||||
for await (const [summary] of stream) {
|
||||
if (canceled) return;
|
||||
|
||||
// join the chunk if it's an array of strings or string|undefined
|
||||
// console.log(summary[0]);
|
||||
setSummary(summary ?? '');
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [currentSpaceId, currentPageId, targetSpaceId, targetPageId]);
|
||||
|
||||
return <p>{summary.length > 1 ? summary : "Loading..."}</p>;
|
||||
}
|
||||
@@ -5,12 +5,15 @@ import { useLanguage } from '@/intl/client';
|
||||
import { tString } from '@/intl/translate';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Button } from '../primitives';
|
||||
import { AIPageNextRecommendedPages } from './AIPageNextRecommendedPages';
|
||||
import { useAdaptivePane } from './state';
|
||||
|
||||
export function AdaptivePane() {
|
||||
export function AdaptivePane(props: { spaceId: string; revisionId: string; pageId: string }) {
|
||||
const language = useLanguage();
|
||||
const { opened, open, close } = useAdaptivePane();
|
||||
|
||||
const {spaceId, revisionId, pageId} = props;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (opened) {
|
||||
document.body.classList.add('adaptive-pane');
|
||||
@@ -48,6 +51,9 @@ export function AdaptivePane() {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<AIPageNextRecommendedPages spaceId={spaceId} revisionId={revisionId} pageId={pageId} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
'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* streamLinkPageSummary({
|
||||
currentSpaceId,
|
||||
currentPageId,
|
||||
targetSpaceId,
|
||||
targetPageId,
|
||||
previousPageIds,
|
||||
}: {
|
||||
currentSpaceId: string;
|
||||
currentPageId: string;
|
||||
targetSpaceId: string;
|
||||
targetPageId: string;
|
||||
previousPageIds: string[];
|
||||
}) {
|
||||
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
||||
|
||||
const [{ stream }, context] = await Promise.all([
|
||||
streamGenerateObject(
|
||||
baseContext,
|
||||
{
|
||||
organizationId: siteURLData.organization,
|
||||
siteId: siteURLData.site,
|
||||
},
|
||||
{
|
||||
schema: z.object({
|
||||
summary: z.array(z.string().describe('The summary of the target page')),
|
||||
// questions: z.array(z.string().describe('The questions to sea')).max(3),
|
||||
}),
|
||||
messages: [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `You are a documentation navigator, tasked with generating summaries of articles the user might navigate to next.
|
||||
Given a user's current article and the target article the user is inspecting, respond with a one-sentence summary of the target article. Make sure to tailor the summary to the user's current article, for example by examining the paragraph the link to the target article is mentioned in.
|
||||
Do not repeat the article's title and description, because these are already shown to the user. Avoid the passive voice, and don't reference the article itself.`,
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: 'Pages in the documentation:',
|
||||
attachments: [
|
||||
{
|
||||
type: 'pages',
|
||||
spaceId: currentSpaceId,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(previousPageIds.length > 0
|
||||
? [
|
||||
{
|
||||
role: AIMessageRole.User,
|
||||
content: `The user has visited the following pages: ${previousPageIds.join(', ')}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
role: AIMessageRole.User,
|
||||
content: `The user is currently on page ID ${currentPageId}, the content of the page is:`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page',
|
||||
spaceId: currentSpaceId,
|
||||
pageId: currentPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.User,
|
||||
content: `The user is looking at page ID ${targetPageId}, the content of the page is:`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page',
|
||||
spaceId: targetSpaceId,
|
||||
pageId: targetPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
),
|
||||
fetchServerActionSiteContext(baseContext),
|
||||
]);
|
||||
|
||||
const emitted = new Set<string>();
|
||||
for await (const value of stream) {
|
||||
const summary = value.summary;
|
||||
if (!summary) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield summary;
|
||||
|
||||
// for (const pageId of pages) {
|
||||
// if (!pageId) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// if (emitted.has(pageId)) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// emitted.add(pageId);
|
||||
|
||||
// const page = resolvePageId(context.pages, pageId);
|
||||
// if (!page) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// yield {
|
||||
// title: page.page.title,
|
||||
// href: context.linker.toPathForPage({
|
||||
// pages: context.pages,
|
||||
// page: page.page,
|
||||
// }),
|
||||
// };
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ 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 '../Adaptive/AIPageLinkSummary';
|
||||
import { Button, StyledLink } from '../primitives';
|
||||
import type { InlineProps } from './Inline';
|
||||
import { Inlines } from './Inlines';
|
||||
@@ -139,20 +140,25 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{!isExternal ? (
|
||||
{!isExternal &&
|
||||
context.contentContext &&
|
||||
'page' in context.contentContext &&
|
||||
inline.data.ref.kind === 'page' ? (
|
||||
<div className="border-tint-subtle border-t bg-tint p-4">
|
||||
<div className="mb-1 flex items-center gap-1 font-semibold text-tint text-xs uppercase leading-tight tracking-wide">
|
||||
<Icon icon="sparkle" className="size-3" />
|
||||
<h6 className="text-tint">AI Summary</h6>
|
||||
</div>
|
||||
<p>
|
||||
This integration synchronizes your docs with the code
|
||||
repository so that your API spec updates in GitBook mirror
|
||||
any changes made in GitHub or GitLab, keeping your content
|
||||
aligned with your code.
|
||||
</p>
|
||||
<AIPageLinkSummary
|
||||
currentPageId={context.contentContext.page.id}
|
||||
currentSpaceId={context.contentContext.space.id}
|
||||
targetPageId={inline.data.ref.page}
|
||||
targetSpaceId={
|
||||
inline.data.ref.space ?? context.contentContext.space.id
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="-m-2 mt-0 flex flex-col rounded-md p-2 text-sm">
|
||||
{/* <div className="-m-2 mt-0 flex flex-col rounded-md p-2 text-sm">
|
||||
<details className="-mx-2 rounded-md px-2 py-1 transition-colors has-[summary:hover]:bg-tint-hover">
|
||||
<summary className="flex items-center gap-1 text-tint hover:cursor-pointer">
|
||||
<Icon
|
||||
@@ -190,21 +196,7 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
It works with GitHub and GitLab.
|
||||
</div>
|
||||
</details>
|
||||
<div className="mt-2 flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
className="w-full rounded-md px-4 py-1 text-sm ring-1 ring-tint-subtle"
|
||||
placeholder="Ask AI about this page"
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
label="Ask question"
|
||||
iconOnly={true}
|
||||
icon="paper-plane-alt"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -74,7 +74,7 @@ export async function SitePage(props: SitePageProps) {
|
||||
) : 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">
|
||||
<AdaptivePane />
|
||||
<AdaptivePane spaceId={context.space.id} revisionId={context.revisionId} pageId={page.id} />
|
||||
<PageAside
|
||||
page={page}
|
||||
document={document}
|
||||
|
||||
@@ -28,6 +28,8 @@ export interface ResolvedContentRef {
|
||||
subText?: string;
|
||||
/** Icon associated with it */
|
||||
icon?: React.ReactNode;
|
||||
/** ID of the content ref */
|
||||
id?: string;
|
||||
/** Emoji associated with the reference */
|
||||
emoji?: string;
|
||||
/** The content ref's ancestors */
|
||||
@@ -175,6 +177,7 @@ export async function resolveContentRef(
|
||||
ancestors: ancestors,
|
||||
emoji,
|
||||
icon,
|
||||
id: page.id,
|
||||
active: !anchor && page.id === activePage?.id,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user