Remove other AI experiments (for now)

This commit is contained in:
Zeno Kapitein
2025-04-03 13:48:35 +02:00
parent b1abbf603b
commit cc26fb34aa
12 changed files with 1 additions and 640 deletions
@@ -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,
@@ -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,
}),
};
}
}
}
@@ -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}