Skip to content

Commit e054805

Browse files
committed
Initial version
1 parent cbd768a commit e054805

File tree

12 files changed

+443
-186
lines changed

12 files changed

+443
-186
lines changed

bun.lock

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4022,7 +4022,7 @@
40224022

40234023
"gaxios/node-fetch": ["[email protected]", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
40244024

4025-
"gitbook-v2/next": ["[email protected]", "", { "dependencies": { "@next/env": "15.3.0-canary.37", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.0-canary.37", "@next/swc-darwin-x64": "15.3.0-canary.37", "@next/swc-linux-arm64-gnu": "15.3.0-canary.37", "@next/swc-linux-arm64-musl": "15.3.0-canary.37", "@next/swc-linux-x64-gnu": "15.3.0-canary.37", "@next/swc-linux-x64-musl": "15.3.0-canary.37", "@next/swc-win32-arm64-msvc": "15.3.0-canary.37", "@next/swc-win32-x64-msvc": "15.3.0-canary.37", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-K9BJZoFYnp+WqR7jYSCzrBJHOwTXJOED964z0En7qszrjF2HXfa24W2bFAMBwK56JD+h4lUyhpbgBoDiDN+vnA=="],
4025+
"gitbook-v2/next": ["[email protected]", "", { "dependencies": { "@next/env": "15.3.0-canary.42", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.0-canary.42", "@next/swc-darwin-x64": "15.3.0-canary.42", "@next/swc-linux-arm64-gnu": "15.3.0-canary.42", "@next/swc-linux-arm64-musl": "15.3.0-canary.42", "@next/swc-linux-x64-gnu": "15.3.0-canary.42", "@next/swc-linux-x64-musl": "15.3.0-canary.42", "@next/swc-win32-arm64-msvc": "15.3.0-canary.42", "@next/swc-win32-x64-msvc": "15.3.0-canary.42", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-f1KQC+GOE72/MYvxEedYY+jpZch3pzNrz51euqt87xWGiRlIngO2VQEbFDwQHZwvzkOtKmC1lDd2Clkd5sLsTQ=="],
40264026

40274027
"global-dirs/ini": ["[email protected]", "", {}, "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="],
40284028

@@ -4890,23 +4890,23 @@
48904890

48914891
"gaxios/https-proxy-agent/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
48924892

4893-
"gitbook-v2/next/@next/env": ["@next/[email protected].37", "", {}, "sha512-NoG8pEk34zpIphk7vSTZ6zs3rj33jtxWO5E2o+bCE8gjFQunChNnRWP1OPo2cATmnc64aJKcvdX5uOuEtAvoUw=="],
4893+
"gitbook-v2/next/@next/env": ["@next/[email protected].42", "", {}, "sha512-J/lYYosUhRJwLejxEkdXoc8NOhJKzMA/aUTQaXD1qGzMWDkvQ5FisO336WMKHRMIVXf1yu2G4JMKOZJ50Qhq+w=="],
48944894

4895-
"gitbook-v2/next/@next/swc-darwin-arm64": ["@next/[email protected].37", "", { "os": "darwin", "cpu": "arm64" }, "sha512-H+gorYP1jSJCmPDpTlQIyLS6GXkA6WZ74H7lxHeXWdAWyP7zv/MmxuhPmsD/RFzYEwZNDSk9jVFpYcOvfWK1Hw=="],
4895+
"gitbook-v2/next/@next/swc-darwin-arm64": ["@next/[email protected].42", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9RlirdgHKaoI97IfVqXlRxhZNuqK9IuWdEu5xZ7fLphYKF+HKAMuqIO1/z53Bl4CSRM7DDpRiyXAyqDJ4VBr7g=="],
48964896

4897-
"gitbook-v2/next/@next/swc-darwin-x64": ["@next/[email protected].37", "", { "os": "darwin", "cpu": "x64" }, "sha512-zul0GsE7SGga8DdBWHRYzdQC5WST1RMmVJRxutvqCqcm7R/GosIzibrEFE5AkW7XMh6bZCO71r4zKPehTuWFcg=="],
4897+
"gitbook-v2/next/@next/swc-darwin-x64": ["@next/[email protected].42", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZAUP4zGtED0mzFYZprch/Pqz3UCnOAee/IdHsKl7uVlmnUyX43I6dDDmk1Srae5RlLu1pVdUkT2tJfT1JKo06w=="],
48984898

4899-
"gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/[email protected].37", "", { "os": "linux", "cpu": "arm64" }, "sha512-ucAOG0sMG2MkNbGTFAI0qncbkci0VE34xjwcpxUT7LUv/rWG83oVOcxcGY5xVbz9V6MpPCxr62FV3I5h6t+w0w=="],
4899+
"gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/[email protected].42", "", { "os": "linux", "cpu": "arm64" }, "sha512-ttgFELQ0oebZNDQweq0jh7ixk8kfUeC6xr3qzjF0eqQygy+DuhEjAzDYZsfCi6G1ToRXJ8+spkZS7Lhv6GNMqw=="],
49004900

4901-
"gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/[email protected].37", "", { "os": "linux", "cpu": "arm64" }, "sha512-hVnST5YroTumu1y+UB4HXN2mbmTs5VWqsr8KmQonAc7+Uu9hvjSb79cpCD5B97h0TBb34Z2vr90hN9w0Q06PFA=="],
4901+
"gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/[email protected].42", "", { "os": "linux", "cpu": "arm64" }, "sha512-XVhtQ6RMbnQMxrm0nWJ5lOrr/48U+Gv8A5mOFwh1URiq9Ur1QBHIzdYJRUFyoN3n0+qSplOyqCzhff3YRauNRQ=="],
49024902

4903-
"gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/[email protected].37", "", { "os": "linux", "cpu": "x64" }, "sha512-p7ArUaiTRm5pSqHg60UtJbZUzsOU90idibVRPgnXd+0Kec7n6TClE3pWcIFow9by0bpSi6kmbNmkHf61i0Kerw=="],
4903+
"gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/[email protected].42", "", { "os": "linux", "cpu": "x64" }, "sha512-ycfjoWE9vxUsZc6LW27aiqiq0m7OtjVz4Ptd9gW5ya8YJvhSWK/0kXo9CVadIFMlX4xJpKl194bZZCJrbD3pvA=="],
49044904

4905-
"gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/[email protected].37", "", { "os": "linux", "cpu": "x64" }, "sha512-Egi3swoN3pbseJ3oRGFYs94ICEjmzmY+exWfNlpycjBMWp1m/QzIcIxHTdz2/Lqe5SNGVMrJ8uEDNAD2knqgvw=="],
4905+
"gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/[email protected].42", "", { "os": "linux", "cpu": "x64" }, "sha512-jr5CIZ6C1QJg2AJQY/vCELLR0EH1DyL+cRfPTDHC5lt2vI3AFsUO6n3rmN0rtOZuMQG4zH3ZrRgMHcZaX5aaDQ=="],
49064906

4907-
"gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/[email protected].37", "", { "os": "win32", "cpu": "arm64" }, "sha512-gOE0lBeS/ztrSHnXimEp9D5OOTN86moSfMgC05ei/UJSFyT7YEZa4FRYRDzctNEilRJC2DBnxjEpeD+JhDuf6A=="],
4907+
"gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/[email protected].42", "", { "os": "win32", "cpu": "arm64" }, "sha512-0Qfe9yKHExdVDQd5NKpGvWYGB0Uz45r4OvxjpOyy5QMZ5j/wzg1BhgM2EfoXoEiBrzeEXnZ2MTk14GwS+BzQLQ=="],
49084908

4909-
"gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/[email protected].37", "", { "os": "win32", "cpu": "x64" }, "sha512-J/SjJYN1Nj/olRC0ykfDNKKoxJss4a3Zf340UeyHbmbqeKHvGbT/H0QFMx+3DT+Sqar3PqbyZBLbxpE/QYct1w=="],
4909+
"gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/[email protected].42", "", { "os": "win32", "cpu": "x64" }, "sha512-SJ5Qj+2duEuwILQLAXH12J74XqJMiulC/VC5+f64FurqC34ESVsgtgzZQAfhkDmp8B/8D24wgL0LTivsVO1vbw=="],
49104910

49114911
"gitbook-v2/next/postcss": ["[email protected]", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
49124912

packages/gitbook-v2/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
},
2424
"scripts": {
2525
"generate": "rm -rf ./public && cp -r ../gitbook/public ./public",
26-
"dev:v2": "env-cmd --silent -f ../../.env.local next --turbopack",
26+
"dev:v2": "env-cmd --silent -f ../../.env.local next",
2727
"build": "next build",
2828
"build:v2": "next build",
2929
"start": "next start",

packages/gitbook-v2/src/lib/data/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,7 @@ async function* streamAIResponse(
998998
input: params.input,
999999
output: params.output,
10001000
model: params.model,
1001+
tools: params.tools,
10011002
});
10021003

10031004
for await (const event of res) {

packages/gitbook-v2/src/lib/data/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,5 +189,6 @@ export interface GitBookDataFetcher {
189189
input: api.AIMessageInput[];
190190
output: api.AIOutputFormat;
191191
model: api.AIModel;
192+
tools?: api.AIToolCapabilities;
192193
}): AsyncGenerator<api.AIStreamResponse, void, unknown>;
193194
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use client';
2+
3+
import { tcls } from '@/lib/tailwind';
4+
import { Icon, type IconName } from '@gitbook/icons';
5+
import { useEffect } from 'react';
6+
import { useState } from 'react';
7+
import { useVisitedPages } from '../Insights';
8+
import { usePageContext } from '../PageContext';
9+
import { streamPageJourneySuggestions } from './server-actions';
10+
11+
export function AIPageJourneySuggestions(props: { spaces: { id: string; title: string }[] }) {
12+
const { spaces } = props;
13+
14+
const currentPage = usePageContext();
15+
16+
// const language = useLanguage();
17+
const visitedPages = useVisitedPages((state) => state.pages);
18+
const [journeys, setJourneys] = useState<({ label?: string; icon?: string } | undefined)[]>([]);
19+
20+
useEffect(() => {
21+
let canceled = false;
22+
23+
(async () => {
24+
const stream = await streamPageJourneySuggestions({
25+
currentPage: {
26+
id: currentPage.pageId,
27+
title: currentPage.title,
28+
},
29+
currentSpace: {
30+
id: currentPage.spaceId,
31+
},
32+
allSpaces: spaces,
33+
visitedPages,
34+
});
35+
36+
for await (const journeys of stream) {
37+
if (canceled) return;
38+
setJourneys(journeys);
39+
}
40+
})();
41+
42+
return () => {
43+
canceled = true;
44+
};
45+
}, [currentPage.pageId, currentPage.spaceId, visitedPages, spaces]);
46+
47+
const shimmerBlocks = [
48+
'[animation-delay:-.2s]',
49+
'[animation-delay:-.4s]',
50+
'[animation-delay:-.6s]',
51+
'[animation-delay:-.8s]',
52+
];
53+
54+
return (
55+
<div className="grid w-72 grid-cols-2 gap-2 text-sm">
56+
{shimmerBlocks.map((block, i) =>
57+
journeys[i]?.icon ? (
58+
<div
59+
// biome-ignore lint/suspicious/noArrayIndexKey: The index is the only identifier available, since we don't know the content of the block until it's loaded in.
60+
key={i}
61+
className="flex animate-fadeIn flex-col items-center justify-center gap-2 rounded border border-tint px-2 py-4 text-center [animation-delay:.2s] [animation-fill-mode:both]"
62+
>
63+
<Icon
64+
icon={journeys[i].icon as IconName}
65+
className="size-4 text-tint-subtle"
66+
/>
67+
{journeys[i].label}
68+
</div>
69+
) : (
70+
<div
71+
// biome-ignore lint/suspicious/noArrayIndexKey: The index is the only identifier available, since we don't know the content of the block until it's loaded in.
72+
key={i}
73+
className={tcls(
74+
'h-24 animate-pulse rounded-md straight-corners:rounded-none border border-tint-subtle',
75+
block
76+
)}
77+
/>
78+
)
79+
)}
80+
</div>
81+
);
82+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { SiteStructure } from '@gitbook/api';
2+
import type { GitBookSiteContext } from '@v2/lib/context';
3+
import { AIPageJourneySuggestions } from './AIPageJourneySuggestions';
4+
5+
export function AdaptivePane(props: { context: GitBookSiteContext }) {
6+
const { context } = props;
7+
8+
return (
9+
<div>
10+
<AIPageJourneySuggestions spaces={getSpaces(context.structure)} />
11+
</div>
12+
);
13+
}
14+
15+
function getSpaces(structure: SiteStructure) {
16+
if (structure.type === 'siteSpaces') {
17+
return structure.structure.map((siteSpace) => ({
18+
id: siteSpace.space.id,
19+
title: siteSpace.space.title,
20+
}));
21+
}
22+
23+
const sections = structure.structure.flatMap((item) =>
24+
item.object === 'site-section-group' ? item.sections : item
25+
);
26+
27+
return sections.flatMap((section) =>
28+
section.siteSpaces.map((siteSpace) => ({
29+
id: siteSpace.space.id,
30+
title: siteSpace.space.title,
31+
}))
32+
);
33+
}

packages/gitbook/src/components/Adaptive/server-actions/api.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
'use server';
2-
import { type AIMessageInput, AIModel, type AIStreamResponse } from '@gitbook/api';
2+
import {
3+
type AIMessageInput,
4+
AIModel,
5+
type AIStreamResponse,
6+
type AIToolCapabilities,
7+
} from '@gitbook/api';
38
import type { GitBookBaseContext } from '@v2/lib/context';
49
import { EventIterator } from 'event-iterator';
510
import type { MaybePromise } from 'p-map';
@@ -47,11 +52,13 @@ export async function streamGenerateObject<T>(
4752
schema,
4853
messages,
4954
model = AIModel.Fast,
55+
tools = {},
5056
}: {
5157
schema: z.ZodSchema<T>;
5258
messages: AIMessageInput[];
5359
model?: AIModel;
5460
previousResponseId?: string;
61+
tools?: AIToolCapabilities;
5562
}
5663
) {
5764
const rawStream = context.dataFetcher.streamAIResponse({
@@ -62,12 +69,13 @@ export async function streamGenerateObject<T>(
6269
type: 'object',
6370
schema: zodToJsonSchema(schema),
6471
},
72+
tools,
6573
model,
6674
});
6775

6876
let json = '';
6977
return parseResponse<DeepPartial<T>>(rawStream, (event) => {
70-
if (event.type === 'response_object') {
78+
if (event.type === 'response_object' && event.jsonChunk) {
7179
json += event.jsonChunk;
7280

7381
const parsed = partialJson.parse(json, partialJson.ALL);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './streamLinkPageSummary';
2+
export * from './streamPageJourneySuggestions';
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
'use server';
2+
import { getV1BaseContext } from '@/lib/v1';
3+
import { isV2 } from '@/lib/v2';
4+
import { AIMessageRole } from '@gitbook/api';
5+
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
6+
import { getServerActionBaseContext } from '@v2/lib/server-actions';
7+
import { z } from 'zod';
8+
import { streamGenerateObject } from './api';
9+
10+
/**
11+
* Get a summary of a page, in the context of another page
12+
*/
13+
export async function* streamPageJourneySuggestions({
14+
currentPage,
15+
currentSpace,
16+
allSpaces,
17+
visitedPages,
18+
}: {
19+
currentPage: {
20+
id: string;
21+
title: string;
22+
};
23+
currentSpace: {
24+
id: string;
25+
// title: string;
26+
};
27+
allSpaces: {
28+
id: string;
29+
title: string;
30+
}[];
31+
visitedPages?: Array<{ spaceId: string; pageId: string }>;
32+
}) {
33+
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
34+
const siteURLData = await getSiteURLDataFromMiddleware();
35+
36+
const { stream } = await streamGenerateObject(
37+
baseContext,
38+
{
39+
organizationId: siteURLData.organization,
40+
siteId: siteURLData.site,
41+
},
42+
{
43+
schema: z.object({
44+
journeys: z
45+
.array(
46+
z.object({
47+
label: z.string().describe('The label of the journey.'),
48+
icon: z
49+
.string()
50+
.describe(
51+
'The icon of the journey. Use an icon from FontAwesome, stripping the `fa-`. Examples: rocket-launch, tennis-ball, cat'
52+
),
53+
})
54+
)
55+
.describe('The possible journeys to take through the documentation.')
56+
.max(4),
57+
}),
58+
tools: {
59+
getPages: true,
60+
getPageContent: true,
61+
},
62+
messages: [
63+
{
64+
role: AIMessageRole.Developer,
65+
content:
66+
"You are a knowledge navigator. Given the user's visited pages and the documentation's table of contents, suggest a named journey through the documentation. A journey is a list of pages that are related to each other. A journey's label starts with a verb and has a clear subject. Use sentence case (so only capitalize the first letter of the first word). Be concise and use short words to fit in the label. For example, use 'docs' instead of 'documentation'. Try to pick out specific journeys, not too generic.",
67+
},
68+
{
69+
role: AIMessageRole.Developer,
70+
content: `The user is in space "${currentSpace.title}"`,
71+
},
72+
{
73+
role: AIMessageRole.Developer,
74+
content: `Other spaces in the documentation are: ${allSpaces
75+
.map(
76+
(space) => `
77+
- "${space.title}" (ID ${space.id})`
78+
)
79+
.join('\n')}
80+
81+
Feel free to create journeys across spaces.`,
82+
},
83+
{
84+
role: AIMessageRole.Developer,
85+
content: `The current page is: "${currentPage.title}" (ID ${currentPage.id}). You can use the getPageContent tool to get the content of any relevant links to include in the journey. Only follow links to pages.`,
86+
attachments: [
87+
{
88+
type: 'page' as const,
89+
spaceId: currentSpace.id,
90+
pageId: currentPage.id,
91+
},
92+
],
93+
},
94+
...(visitedPages && visitedPages.length > 0
95+
? [
96+
{
97+
role: AIMessageRole.Developer,
98+
content: `The user's visited pages are: ${visitedPages.map((page) => page.pageId).join(', ')}. The content of the last 5 pages are included below.`,
99+
attachments: visitedPages.slice(0, 5).map((page) => ({
100+
type: 'page' as const,
101+
spaceId: page.spaceId,
102+
pageId: page.pageId,
103+
})),
104+
},
105+
]
106+
: []),
107+
],
108+
}
109+
);
110+
111+
// const emitted = new Set<string>();
112+
for await (const value of stream) {
113+
const journeys = value.journeys;
114+
if (!journeys) {
115+
continue;
116+
}
117+
118+
// for (const journey of journeys) {
119+
// if (emitted.has(journey)) {
120+
// continue;
121+
// }
122+
123+
// emitted.add(journey);
124+
// yield journey;
125+
// }
126+
yield journeys;
127+
}
128+
}

0 commit comments

Comments
 (0)