mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-25 03:42:30 +00:00
Remove other AI experiments (for now)
This commit is contained in:
@@ -1,60 +0,0 @@
|
||||
'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';
|
||||
|
||||
/**
|
||||
* Show a list of recommended pages to read next.
|
||||
*/
|
||||
export function AIPageNextRecommendedPages(props: {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
pageId: string;
|
||||
}) {
|
||||
const { spaceId, revisionId, pageId } = props;
|
||||
|
||||
const [pages, setPages] = useState<{ title: string; href: string; icon?: string }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
|
||||
setPages([]);
|
||||
|
||||
(async () => {
|
||||
const stream = await streamNextRecommendedPages({
|
||||
spaceId,
|
||||
revisionId,
|
||||
pageId,
|
||||
previousPageIds: [],
|
||||
});
|
||||
|
||||
for await (const page of stream) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPages((pages) => [...pages, page]);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [spaceId, revisionId, pageId]);
|
||||
|
||||
return (
|
||||
<div className={tcls('flex flex-col gap-2')}>
|
||||
{pages.map((page) => {
|
||||
return (
|
||||
<Link className='flex items-center gap-3' key={page.href} href={page.href}>
|
||||
<Icon className="size-4 text-primary-subtle" icon={page.icon} />
|
||||
{page.title}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link } from '../primitives';
|
||||
import { streamPageRecommendedQuestions } from './server-actions';
|
||||
|
||||
/**
|
||||
* Show a list of recommended questions for a page.
|
||||
*/
|
||||
export function AIPageRecommendedQuestions(props: {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
pageId: string;
|
||||
}) {
|
||||
const { spaceId, revisionId, pageId } = props;
|
||||
|
||||
const [questions, setQuestions] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
|
||||
setQuestions([]);
|
||||
|
||||
(async () => {
|
||||
const stream = await streamPageRecommendedQuestions({ spaceId, revisionId, pageId });
|
||||
for await (const question of stream) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setQuestions((questions) => [...questions, question]);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [spaceId, revisionId, pageId]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={tcls(
|
||||
'flex flex-row flex-wrap gap-2',
|
||||
'max-w-3xl',
|
||||
'mx-auto',
|
||||
'page-api-block:ml-0'
|
||||
)}
|
||||
>
|
||||
{questions.map((question) => {
|
||||
const urlParams = new URLSearchParams();
|
||||
urlParams.set('q', question);
|
||||
urlParams.set('ask', 'true');
|
||||
|
||||
return (
|
||||
<Link key={question} href={`?${urlParams.toString()}`}>
|
||||
{question}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Icon, type IconName } from '@gitbook/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { streamPageTopics } from './server-actions/streamPageTopics';
|
||||
|
||||
/**
|
||||
* Show a list of recommended pages to read next.
|
||||
*/
|
||||
export function AIPageTopics(props: {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
pageId: string;
|
||||
}) {
|
||||
const { spaceId, revisionId, pageId } = props;
|
||||
|
||||
const [topics, setTopics] = useState<{ name?: string; icon?: string }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
|
||||
setTopics([]);
|
||||
|
||||
(async () => {
|
||||
const stream = await streamPageTopics({
|
||||
spaceId,
|
||||
revisionId,
|
||||
pageId,
|
||||
previousPageIds: [],
|
||||
});
|
||||
|
||||
for await (const topics of stream) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTopics(topics);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [spaceId, revisionId, pageId]);
|
||||
|
||||
return (
|
||||
<div className={tcls('grid grid-flow-row grid-cols-2 gap-2')}>
|
||||
{topics.map((topic) => (
|
||||
<div
|
||||
key={topic.name}
|
||||
className='flex flex-col items-center justify-center gap-2 rounded-md border border-tint-subtle p-2 text-center transition-colors hover:cursor-pointer hover:bg-primary-subtle hover:text-primary'
|
||||
>
|
||||
{topic.icon ? (
|
||||
<Icon
|
||||
icon={topic.icon as IconName}
|
||||
className="size-4 text-primary-subtle"
|
||||
/>
|
||||
) : null}{' '}
|
||||
{topic.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
|
||||
import { useLanguage } from '@/intl/client';
|
||||
import { tString } from '@/intl/translate';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Button } from '../primitives';
|
||||
import { AIPageNextRecommendedPages } from './AIPageNextRecommendedPages';
|
||||
import { AIPageTopics } from './AIPageTopics';
|
||||
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;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (opened) {
|
||||
document.body.classList.add('adaptive-pane');
|
||||
} else {
|
||||
document.body.classList.remove('adaptive-pane');
|
||||
}
|
||||
}, [opened]);
|
||||
|
||||
const pages = [
|
||||
'GitBook Documentation',
|
||||
'GitHub & GitLab Sync',
|
||||
'Troubleshooting',
|
||||
'Version control',
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={tcls('w-80 flex-col text-sm', opened ? 'flex' : 'hidden')}>
|
||||
<div className="sticky top-28 my-4 flex flex-col rounded-md bg-tint-subtle theme-gradient:bg-tint-1/1 p-4">
|
||||
<div className="flex items-center gap-2 pb-2">
|
||||
<div className="flex grow flex-col gap-1">
|
||||
<h2
|
||||
className={tcls(
|
||||
'flex-1',
|
||||
'text-xs',
|
||||
'tracking-wide',
|
||||
'font-semibold',
|
||||
'uppercase'
|
||||
)}
|
||||
>
|
||||
Recommended
|
||||
</h2>
|
||||
<p className="text-tint text-xs">Based on your context</p>
|
||||
</div>
|
||||
<Button
|
||||
iconOnly
|
||||
variant="blank"
|
||||
size="medium"
|
||||
icon={opened ? 'chevrons-right' : 'chevrons-left'}
|
||||
label={
|
||||
opened
|
||||
? tString(language, 'adaptive_pane_close')
|
||||
: tString(language, 'adaptive_pane_open')
|
||||
}
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-6">
|
||||
<AIPageTopics spaceId={spaceId} pageId={pageId} revisionId={revisionId} />
|
||||
<AIPageNextRecommendedPages
|
||||
spaceId={spaceId}
|
||||
pageId={pageId}
|
||||
revisionId={revisionId}
|
||||
/>
|
||||
{/* <AIPageRecommendedQuestions
|
||||
spaceId={spaceId}
|
||||
pageId={pageId}
|
||||
revisionId={revisionId}
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1 @@
|
||||
export * from './AIPageRecommendedQuestions';
|
||||
export * from './AIPageNextRecommendedPages';
|
||||
export * from './AdaptivePane';
|
||||
export * from './AIPageLinkSummary';
|
||||
|
||||
@@ -60,7 +60,6 @@ export async function streamGenerateObject<T>(
|
||||
input: messages,
|
||||
output: {
|
||||
type: 'object',
|
||||
// @ts-expect-error
|
||||
schema: zodToJsonSchema(schema),
|
||||
},
|
||||
model,
|
||||
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
'use server';
|
||||
|
||||
import { resolvePageId } from '@/lib/pages';
|
||||
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 list of pages that are recommended to be read next.
|
||||
*/
|
||||
export async function* streamNextRecommendedPages({
|
||||
spaceId,
|
||||
revisionId,
|
||||
previousPageIds,
|
||||
pageId,
|
||||
}: {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
previousPageIds: string[];
|
||||
pageId: 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({
|
||||
pages: z.array(z.string().describe('The id of the page')).max(5),
|
||||
}),
|
||||
messages: [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content:
|
||||
'You are an AI to help users navigate and get the most out of the documentation. Using the table of contents and a list of pages that the user has visited, generate a list of pages that are relevant to read next.',
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.User,
|
||||
content: 'Pages in the documentation:',
|
||||
attachments: [
|
||||
{
|
||||
type: 'pages',
|
||||
spaceId,
|
||||
revisionId,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(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 the page "${pageId}", the content of the page is:`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page',
|
||||
spaceId,
|
||||
revisionId,
|
||||
pageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
),
|
||||
fetchServerActionSiteContext(baseContext),
|
||||
]);
|
||||
|
||||
const emitted = new Set<string>();
|
||||
for await (const value of stream) {
|
||||
const pages = value.pages;
|
||||
if (!pages) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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 {
|
||||
icon: page.page.icon ?? page.page.emoji,
|
||||
title: page.page.title,
|
||||
href: context.linker.toPathForPage({
|
||||
pages: context.pages,
|
||||
page: page.page,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
'use server';
|
||||
|
||||
import { getV1BaseContext } from '@/lib/v1';
|
||||
import { isV2 } from '@/lib/v2';
|
||||
import { AIMessageRole } from '@gitbook/api';
|
||||
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
|
||||
import { getServerActionBaseContext } from '@v2/lib/server-actions';
|
||||
import { z } from 'zod';
|
||||
import { streamGenerateObject } from './api';
|
||||
|
||||
/**
|
||||
* Get a list of questions that are recommended to be asked on a page.
|
||||
*/
|
||||
export async function* streamPageRecommendedQuestions({
|
||||
spaceId,
|
||||
revisionId,
|
||||
pageId,
|
||||
}: {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
pageId: string;
|
||||
}) {
|
||||
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
||||
|
||||
const { stream } = await streamGenerateObject(
|
||||
baseContext,
|
||||
{
|
||||
organizationId: siteURLData.organization,
|
||||
siteId: siteURLData.site,
|
||||
},
|
||||
{
|
||||
schema: z.object({
|
||||
questions: z.array(z.string()).max(5),
|
||||
}),
|
||||
messages: [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content:
|
||||
'You are an AI to help users navigate and get the most out of the documentation. Based on the content of the page, generate a list of questions that are relevant to ask after reading the page.',
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.User,
|
||||
content: 'Pages in the documentation:',
|
||||
attachments: [
|
||||
{
|
||||
type: 'pages',
|
||||
spaceId,
|
||||
revisionId,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.User,
|
||||
content: 'Content of the current page:',
|
||||
attachments: [
|
||||
{
|
||||
type: 'page',
|
||||
spaceId,
|
||||
revisionId,
|
||||
pageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
const emitted = new Set<string>();
|
||||
for await (const value of stream) {
|
||||
const questions = value.questions;
|
||||
if (!questions) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const question of questions) {
|
||||
if (!question) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (emitted.has(question)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
emitted.add(question);
|
||||
yield question;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
'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 list of pages that are recommended to be read next.
|
||||
*/
|
||||
export async function* streamPageTopics({
|
||||
spaceId,
|
||||
revisionId,
|
||||
previousPageIds,
|
||||
pageId,
|
||||
}: {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
previousPageIds: string[];
|
||||
pageId: 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({
|
||||
topics: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().describe('The recommended topic name'),
|
||||
icon: z
|
||||
.string()
|
||||
.describe(
|
||||
'A fontawesome icon name to describe the topic, without the `fa-` part.'
|
||||
),
|
||||
})
|
||||
)
|
||||
.max(4),
|
||||
}),
|
||||
messages: [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `
|
||||
# Task
|
||||
You are a documentation navigator, tasked with helping the user navigate through documentation.
|
||||
The user is currently reading a page, and is considering next pages to navigate to.
|
||||
Gather topics present in the documentation, and present the relevant topics to the user given their existing page and context. These topics will inform next pages to visit.
|
||||
Topics always contain a verb and a subject. Use sentence case (capitalise first word only).
|
||||
`,
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.User,
|
||||
content: 'Pages in the documentation:',
|
||||
attachments: [
|
||||
{
|
||||
type: 'pages',
|
||||
spaceId,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(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 the page "${pageId}", the content of the page is:`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page',
|
||||
spaceId,
|
||||
pageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
),
|
||||
fetchServerActionSiteContext(baseContext),
|
||||
]);
|
||||
|
||||
// const emitted = new Set<{ id: string, name?: string; icon?: string }>();
|
||||
for await (const value of stream) {
|
||||
const topics = value.topics;
|
||||
if (!topics) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// for (const topic of topics) {
|
||||
// if (!topic) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// // const page = resolvePageId(context.pages, pageId);
|
||||
// if (!topic.name || !topic.icon) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// emitted.has(topic)
|
||||
// yield {
|
||||
// name: topic.name,
|
||||
// icon: topic.icon,
|
||||
// };
|
||||
// }
|
||||
yield topics;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
export const useAdaptivePane = create<{
|
||||
opened: boolean;
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
}>((set) => ({
|
||||
opened: true,
|
||||
|
||||
/**
|
||||
* Open the adaptive pane
|
||||
*/
|
||||
open: () => set({ opened: true }),
|
||||
|
||||
/**
|
||||
* Close the adaptive pane
|
||||
*/
|
||||
close: () => set({ opened: false }),
|
||||
}));
|
||||
@@ -13,7 +13,6 @@ 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';
|
||||
@@ -43,8 +42,6 @@ export function PageAside(props: {
|
||||
}).toString()}`
|
||||
);
|
||||
|
||||
const prevPages = ['GitBook Documentation', 'GitHub & GitLab Sync', 'Troubleshooting'];
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={tcls(
|
||||
@@ -93,9 +90,6 @@ export function PageAside(props: {
|
||||
'page-api-block:xl:max-2xl:h-auto',
|
||||
'page-api-block:xl:max-2xl:my-8',
|
||||
'page-api-block:p-2',
|
||||
|
||||
// When the adaptive pane is open, we hide the aside
|
||||
'adaptive-pane:hidden'
|
||||
)}
|
||||
>
|
||||
{page.layout.outline ? (
|
||||
@@ -123,26 +117,12 @@ export function PageAside(props: {
|
||||
'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>
|
||||
{document ? (
|
||||
<React.Suspense fallback={null}>
|
||||
<PageAsideSections document={document} context={context} />
|
||||
</React.Suspense>
|
||||
) : 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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user