Tweak link summaries

This commit is contained in:
Zeno Kapitein
2025-04-02 15:56:46 +02:00
parent e4cd67ebe2
commit 08e780496b
10 changed files with 196 additions and 163 deletions
@@ -1,5 +1,7 @@
'use client';
import { Icon } from '@gitbook/icons';
import { useEffect, useState } from 'react';
import { Loading } from '../primitives';
import { streamLinkPageSummary } from './server-actions/streamLinkPageSummary';
/**
@@ -11,8 +13,16 @@ export function AIPageLinkSummary(props: {
targetSpaceId: string;
targetPageId: string;
linkPreview?: string;
showTrademark: boolean;
}) {
const { currentSpaceId, currentPageId, targetSpaceId, targetPageId, linkPreview } = props;
const {
currentSpaceId,
currentPageId,
targetSpaceId,
targetPageId,
linkPreview,
showTrademark = true,
} = props;
const [highlight, setHighlight] = useState('');
@@ -45,5 +55,19 @@ export function AIPageLinkSummary(props: {
};
}, [currentSpaceId, currentPageId, targetSpaceId, targetPageId]);
return <p>{highlight.length > 1 ? highlight : "Loading..."}</p>;
return (
<div>
<div className="mb-1 flex items-center gap-1 font-semibold text-tint text-xs uppercase leading-tight tracking-wide">
{showTrademark ? (
<Loading className="size-4" busy={!highlight || highlight.length === 0} />
) : (
<Icon icon="sparkle" className="size-3" />
)}
<h6 className="text-tint">
Page highlight
</h6>
</div>
{highlight}
</div>
);
}
@@ -1,6 +1,7 @@
'use client';
import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons';
import { useEffect, useState } from 'react';
import { Link } from '../primitives';
import { streamNextRecommendedPages } from './server-actions';
@@ -15,7 +16,7 @@ export function AIPageNextRecommendedPages(props: {
}) {
const { spaceId, revisionId, pageId } = props;
const [pages, setPages] = useState<{ title: string; href: string }[]>([]);
const [pages, setPages] = useState<{ title: string; href: string; icon?: string }[]>([]);
useEffect(() => {
let canceled = false;
@@ -45,17 +46,11 @@ export function AIPageNextRecommendedPages(props: {
}, [spaceId, revisionId, pageId]);
return (
<div
className={tcls(
'flex flex-row flex-wrap gap-2',
'max-w-3xl',
'mx-auto',
'page-api-block:ml-0'
)}
>
<div className={tcls('flex flex-col gap-2')}>
{pages.map((page) => {
return (
<Link key={page.href} href={page.href}>
<Link className='flex items-center gap-3' key={page.href} href={page.href}>
<Icon className="size-4" icon={page.icon} />
{page.title}
</Link>
);
@@ -4,15 +4,15 @@ import React from 'react';
import { useLanguage } from '@/intl/client';
import { tString } from '@/intl/translate';
import { tcls } from '@/lib/tailwind';
import Link from 'next/link';
import { Button } from '../primitives';
import { AIPageNextRecommendedPages } from './AIPageNextRecommendedPages';
import { useAdaptivePane } from './state';
export function AdaptivePane(props: { spaceId: string; revisionId: string; pageId: string }) {
const language = useLanguage();
const { opened, open, close } = useAdaptivePane();
const {spaceId, revisionId, pageId} = props;
const { spaceId, revisionId, pageId } = props;
React.useEffect(() => {
if (opened) {
@@ -22,10 +22,17 @@ export function AdaptivePane(props: { spaceId: string; revisionId: string; pageI
}
}, [opened]);
const pages = [
'GitBook Documentation',
'GitHub & GitLab Sync',
'Troubleshooting',
'Version control',
];
return (
<div className={tcls('w-80 flex-col', opened ? 'flex' : 'hidden')}>
<div className="sticky top-28 flex flex-col gap-4 py-4">
<div className="flex items-center gap-2">
<div className={tcls('w-80 flex-col text-sm', opened ? 'flex' : 'hidden')}>
<div className="sticky top-28 my-4 flex flex-col gap-2 rounded-md border border-tint-subtle bg-tint-subtle theme-gradient:bg-tint-1/1 p-4">
<div className='flex items-center gap-2 border-tint-subtle border-b pb-2'>
<h2
className={tcls(
'flex-1',
@@ -35,11 +42,12 @@ export function AdaptivePane(props: { spaceId: string; revisionId: string; pageI
'uppercase'
)}
>
{tString(language, 'adaptive_pane_title')}
History
</h2>
<Button
iconOnly
variant="blank"
size="medium"
icon={opened ? 'chevrons-right' : 'chevrons-left'}
label={
opened
@@ -52,7 +60,18 @@ export function AdaptivePane(props: { spaceId: string; revisionId: string; pageI
/>
</div>
<div>
<AIPageNextRecommendedPages spaceId={spaceId} revisionId={revisionId} pageId={pageId} />
<ol className="-mx-2 flex flex-col gap-0.5 text-tint-subtle">
{pages.map((page) => (
<li key={page}>
<Link
className="block rounded px-2 py-1.5 last:bg-primary-hover hover:bg-primary-hover"
href="#"
>
{page}
</Link>
</li>
))}
</ol>
</div>
</div>
</div>
@@ -131,8 +131,6 @@ The content of the target page is:`,
fetchServerActionSiteContext(baseContext),
]);
console.log(await response);
const emitted = new Set<string>();
for await (const value of stream) {
const highlight = value.highlight;
@@ -104,6 +104,7 @@ export async function* streamNextRecommendedPages({
}
yield {
icon: page.page.icon ?? page.page.emoji,
title: page.page.title,
href: context.linker.toPathForPage({
pages: context.pages,
@@ -20,7 +20,7 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
})
: null;
if (!resolved) {
if (!context.contentContext || !resolved) {
return (
<span title="Broken link" className="underline">
<Inlines
@@ -141,20 +141,24 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
</div>
{!isExternal &&
context.contentContext ? (
'site' in context.contentContext &&
'page' in context.contentContext &&
context.contentContext.site.adaptiveContent?.enabled &&
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>
<AIPageLinkSummary
currentPageId={context.contentContext.page.id}
currentSpaceId={context.contentContext.space.id}
targetPageId={inline.data.ref.page}
targetPageId={
inline.data.ref.page ?? context.contentContext.page.id
}
targetSpaceId={
inline.data.ref.space ?? context.contentContext.space.id
}
linkPreview={`**${resolved.text}**: ${resolved.subText}`}
showTrademark={
context.contentContext.customization.trademark.enabled
}
/>
{/* <div className="-m-2 mt-0 flex flex-col rounded-md p-2 text-sm">
@@ -13,7 +13,7 @@ import urlJoin from 'url-join';
import { getSpaceLanguage, t } from '@/intl/server';
import { getDocumentSections } from '@/lib/document-sections';
import { tcls } from '@/lib/tailwind';
import { AIPageNextRecommendedPages } from '../Adaptive';
import { Ad } from '../Ads';
import { getPDFURLSearchParams } from '../PDF';
import { PageFeedbackForm } from '../PageFeedback';
@@ -42,12 +42,16 @@ export function PageAside(props: {
limit: 100,
}).toString()}`
);
const prevPages = ['GitBook Documentation', 'GitHub & GitLab Sync', 'Troubleshooting'];
return (
<aside
className={tcls(
'group/aside',
'hidden',
'xl:flex',
'text-sm',
// 'page-no-toc:lg:flex',
'flex-col',
'basis-56',
@@ -88,146 +92,132 @@ export function PageAside(props: {
'page-api-block:xl:max-2xl:rounded-md',
'page-api-block:xl:max-2xl:h-auto',
'page-api-block:xl:max-2xl:my-8',
'page-api-block:p-2',
'page-api-block:p-2'
// When the adaptive pane is open, we hide the aside
'adaptive-pane:hidden'
// 'adaptive-pane:hidden'
)}
>
{page.layout.outline ? (
<>
<div
className={tcls(
'hidden',
'page-api-block:xl:max-2xl:flex',
'text-xs',
'tracking-wide',
'font-semibold',
'uppercase',
<div
className={tcls(
'overflow-y-auto',
'overflow-x-visible',
'flex-row',
'items-center',
'gap-2'
)}
>
<Icon icon="block-quote" className={tcls('size-3')} />
{t(language, 'on_this_page')}
<Icon
icon="chevron-down"
className={tcls(
'size-3',
'opacity-6',
'ml-auto',
'page-api-block:xl:max-2xl:group-hover/aside:hidden'
)}
/>
'flex',
'flex-col',
'shrink',
'pb-12',
'sticky',
'lg:top:0',
'site-header:lg:top-16',
'site-header-sections:lg:top-[6.75rem]',
'gap-2',
'pt-8',
'page-api-block:xl:max-2xl:py-0',
// Hide it for api page, until hovered
'page-api-block:xl:max-2xl:hidden',
'page-api-block:xl:max-2xl:group-hover/aside:flex'
)}
>
<div className="flex items-center gap-2 font-semibold text-xs uppercase tracking-wide">
<Icon icon="block-quote" className="size-3" />
On this page
</div>
<div
className={tcls(
'overflow-y-auto',
'overflow-x-visible',
'flex',
'flex-col',
'shrink',
'pb-12',
'sticky',
'lg:top:0',
'site-header:lg:top-16',
'site-header-sections:lg:top-[6.75rem]',
'gap-6',
'pt-8',
'page-api-block:xl:max-2xl:py-0',
// Hide it for api page, until hovered
'page-api-block:xl:max-2xl:hidden',
'page-api-block:xl:max-2xl:group-hover/aside:flex'
)}
>
{document ? (
{document ? (
<div className="-ml-3">
<React.Suspense fallback={null}>
<PageAsideSections document={document} context={context} />
</React.Suspense>
) : null}
<div
className={tcls(
'flex',
'flex-col',
'gap-3',
'sidebar-list-default:px-3',
'border-t',
'first:border-none',
'border-tint-subtle',
'py-4',
'first:pt-0',
'page-api-block:xl:max-2xl:px-3',
'empty:hidden'
)}
>
{withPageFeedback ? (
<React.Suspense fallback={null}>
<PageFeedbackForm pageId={page.id} className={tcls('mt-2')} />
</React.Suspense>
) : null}
{customization.git.showEditLink && space.gitSync?.url && page.git ? (
<div>
<a
href={urlJoin(space.gitSync.url, page.git.path)}
className={tcls(
'flex',
'flex-row',
'items-center',
'text-sm',
'hover:text-tint-strong',
'links-accent:hover:underline',
'links-accent:hover:underline-offset-4',
'links-accent:hover:decoration-[3px]',
'links-accent:hover:decoration-primary-subtle',
'py-2'
)}
>
<Icon
icon={
space.gitSync.installationProvider === 'gitlab'
? 'gitlab'
: 'github'
}
className={tcls('size-4', 'mr-1.5')}
/>
{t(language, 'edit_on_git', getGitSyncName(space))}
</a>
</div>
) : null}
{customization.pdf.enabled ? (
<div>
<a
href={pdfHref}
className={tcls(
'flex',
'flex-row',
'items-center',
'text-sm',
'hover:text-tint-strong',
'links-accent:hover:underline',
'links-accent:hover:underline-offset-4',
'links-accent:hover:decoration-[3px]',
'links-accent:hover:decoration-primary-subtle',
'py-2'
)}
>
<Icon
icon="file-pdf"
className={tcls('size-4', 'mr-1.5')}
/>
{t(language, 'pdf_download')}
</a>
</div>
) : null}
</div>
) : null}
<div className='mt-6 flex items-center gap-2 font-semibold text-xs uppercase tracking-wide'>
<Icon icon="sparkle" className="size-3" />
Next pages
</div>
</>
<AIPageNextRecommendedPages
spaceId={context.space.id}
pageId={page.id}
revisionId={context.revisionId}
/>
<div
className={tcls(
'flex',
'flex-col',
'gap-3',
'sidebar-list-default:px-3',
'border-t',
'first:border-none',
'border-tint-subtle',
'py-4',
'first:pt-0',
'page-api-block:xl:max-2xl:px-3',
'empty:hidden'
)}
>
{withPageFeedback ? (
<React.Suspense fallback={null}>
<PageFeedbackForm pageId={page.id} className={tcls('mt-2')} />
</React.Suspense>
) : null}
{customization.git.showEditLink && space.gitSync?.url && page.git ? (
<div>
<a
href={urlJoin(space.gitSync.url, page.git.path)}
className={tcls(
'flex',
'flex-row',
'items-center',
'text-sm',
'hover:text-tint-strong',
'links-accent:hover:underline',
'links-accent:hover:underline-offset-4',
'links-accent:hover:decoration-[3px]',
'links-accent:hover:decoration-primary-subtle',
'py-2'
)}
>
<Icon
icon={
space.gitSync.installationProvider === 'gitlab'
? 'gitlab'
: 'github'
}
className={tcls('size-4', 'mr-1.5')}
/>
{t(language, 'edit_on_git', getGitSyncName(space))}
</a>
</div>
) : null}
{customization.pdf.enabled ? (
<div>
<a
href={pdfHref}
className={tcls(
'flex',
'flex-row',
'items-center',
'text-sm',
'hover:text-tint-strong',
'links-accent:hover:underline',
'links-accent:hover:underline-offset-4',
'links-accent:hover:decoration-[3px]',
'links-accent:hover:decoration-primary-subtle',
'py-2'
)}
>
<Icon icon="file-pdf" className={tcls('size-4', 'mr-1.5')} />
{t(language, 'pdf_download')}
</a>
</div>
) : null}
</div>
</div>
) : null}
<div
className={tcls(
@@ -11,7 +11,6 @@ import { getPagePath } from '@/lib/pages';
import { isPageIndexable, isSiteIndexable } from '@/lib/seo';
import { getResizedImageURL } from '@v2/lib/images';
import { AdaptivePane } from '../Adaptive';
import { PageClientLayout } from './PageClientLayout';
import { type PagePathParams, fetchPageData, getPathnameParam } from './fetch';
@@ -74,7 +73,6 @@ 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 spaceId={context.space.id} revisionId={context.revisionId} pageId={page.id} />
<PageAside
page={page}
document={document}
@@ -32,7 +32,7 @@ const variantClasses = {
'text-tint',
'ring-0',
'shadow-none',
'hover:bg-tint-hover',
'hover:bg-primary-hover',
'hover:text-primary',
'hover:scale-1',
'hover:shadow-none',
@@ -2,7 +2,10 @@ import type { SVGProps } from 'react';
import { tcls } from '@/lib/tailwind';
export const Loading = (props: Partial<SVGProps<SVGSVGElement>>) => {
export const Loading = ({
busy = true,
...props
}: { busy?: boolean } & SVGProps<SVGSVGElement>) => {
return (
<svg
width="100%"
@@ -14,7 +17,7 @@ export const Loading = (props: Partial<SVGProps<SVGSVGElement>>) => {
{...props}
>
<path
className={tcls('animate-[pathLoading_2s_ease_infinite_forwards]')}
className={tcls(busy ? 'animate-[pathLoading_2s_ease_infinite_forwards]' : 'animate-[pathLoading_2s_ease_forwards]' )}
d="M6 59.5V56.291C6 45.8865 11.5194 36.263 20.5 31.0091V31.0091L60.9857 7.32407C63.4452 5.88525 66.4843 5.86317 68.9643 7.26611L116 33.8734L70.4183 60.2148C67.9468 61.6431 64.9014 61.6462 62.4269 60.223L29.9772 41.5592C19.3106 35.4242 6 43.1236 6 55.4288V64.8776C6 73.4486 10.5708 81.3691 17.9918 85.6575L54.59 106.807C62.0198 111.1 71.1766 111.1 78.6064 106.807L116.364 84.9874C120.074 82.8432 122.36 78.883 122.36 74.5975V59.2647C122.36 57.7248 120.692 56.7626 119.359 57.5331L72.6023 84.5529C68.8874 86.6996 64.309 86.6996 60.5941 84.5529L26 64.5617"
stroke="currentColor"
pathLength="100"
@@ -27,7 +30,8 @@ export const Loading = (props: Partial<SVGProps<SVGSVGElement>>) => {
d="M6 59.5V56.291C6 45.8865 11.5194 36.263 20.5 31.0091V31.0091L60.9857 7.32407C63.4452 5.88525 66.4843 5.86317 68.9643 7.26611L116 33.8734L70.4183 60.2148C67.9468 61.6431 64.9014 61.6462 62.4269 60.223L29.9772 41.5592C19.3106 35.4242 6 43.1236 6 55.4288V64.8776C6 73.4486 10.5708 81.3691 17.9918 85.6575L54.59 106.807C62.0198 111.1 71.1766 111.1 78.6064 106.807L116.364 84.9874C120.074 82.8432 122.36 78.883 122.36 74.5975V59.2647C122.36 57.7248 120.692 56.7626 119.359 57.5331L72.6023 84.5529C68.8874 86.6996 64.309 86.6996 60.5941 84.5529L26 64.5617"
stroke="currentColor"
pathLength="100"
strokeOpacity="0.24"
strokeOpacity={busy ? 0.24 : 1}
className='transition-opacity duration-1000'
fill="none"
strokeWidth="11"
strokeLinecap="round"