diff --git a/bun.lock b/bun.lock
index 96f0f0d20..2577a3e32 100644
--- a/bun.lock
+++ b/bun.lock
@@ -26,7 +26,7 @@
"name": "@gitbook/cache-tags",
"version": "0.3.1",
"dependencies": {
- "@gitbook/api": "^0.115.0",
+ "@gitbook/api": "^0.117.1",
"assert-never": "^1.2.1",
},
"devDependencies": {
@@ -51,7 +51,7 @@
"name": "gitbook",
"version": "0.12.0",
"dependencies": {
- "@gitbook/api": "^0.115.0",
+ "@gitbook/api": "^0.117.1",
"@gitbook/cache-do": "workspace:*",
"@gitbook/cache-tags": "workspace:*",
"@gitbook/colors": "workspace:*",
@@ -143,7 +143,7 @@
"name": "gitbook-v2",
"version": "0.3.0",
"dependencies": {
- "@gitbook/api": "^0.115.0",
+ "@gitbook/api": "^0.117.1",
"@gitbook/cache-tags": "workspace:*",
"@opennextjs/cloudflare": "1.1.0",
"@sindresorhus/fnv1a": "^3.1.0",
@@ -202,7 +202,7 @@
"name": "@gitbook/react-contentkit",
"version": "0.7.0",
"dependencies": {
- "@gitbook/api": "^0.115.0",
+ "@gitbook/api": "^0.117.1",
"@gitbook/icons": "workspace:*",
"classnames": "^2.5.1",
},
@@ -260,7 +260,7 @@
},
"overrides": {
"@codemirror/state": "6.4.1",
- "@gitbook/api": "^0.115.0",
+ "@gitbook/api": "^0.117.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
},
@@ -625,7 +625,7 @@
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
- "@gitbook/api": ["@gitbook/api@0.115.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-Lyj+1WVNnE/Zuuqa/1ZdnUQfUiNE6es89RFK6CJ+Tb36TFwls6mbHKXCZsBwSYyoMYTVK39WQ3Nob6Nw6+TWCA=="],
+ "@gitbook/api": ["@gitbook/api@0.117.1", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-edyi4Y+Zs/9vdU6CvJfwtVSiEWrKzzlp+sIfsyw8WT+mJWnH9CN+o17nxvRWNmAznU+YZm+Qz7itVbG8k+vSkQ=="],
"@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
diff --git a/package.json b/package.json
index 5398e52f9..66b8b8bc9 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"packageManager": "bun@1.2.11",
"overrides": {
"@codemirror/state": "6.4.1",
- "@gitbook/api": "^0.115.0",
+ "@gitbook/api": "^0.117.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
diff --git a/packages/cache-tags/package.json b/packages/cache-tags/package.json
index 8850dafbd..e4c267661 100644
--- a/packages/cache-tags/package.json
+++ b/packages/cache-tags/package.json
@@ -10,7 +10,7 @@
},
"version": "0.3.1",
"dependencies": {
- "@gitbook/api": "^0.115.0",
+ "@gitbook/api": "^0.117.1",
"assert-never": "^1.2.1"
},
"devDependencies": {
diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json
index 0b790a9be..d817f15ac 100644
--- a/packages/gitbook-v2/package.json
+++ b/packages/gitbook-v2/package.json
@@ -3,7 +3,7 @@
"version": "0.3.0",
"private": true,
"dependencies": {
- "@gitbook/api": "^0.115.0",
+ "@gitbook/api": "^0.117.1",
"@gitbook/cache-tags": "workspace:*",
"@opennextjs/cloudflare": "1.1.0",
"@sindresorhus/fnv1a": "^3.1.0",
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index a3b9da821..519c66373 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -16,7 +16,7 @@
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
},
"dependencies": {
- "@gitbook/api": "^0.115.0",
+ "@gitbook/api": "^0.117.1",
"@gitbook/cache-do": "workspace:*",
"@gitbook/cache-tags": "workspace:*",
"@gitbook/colors": "workspace:*",
diff --git a/packages/gitbook/src/components/Adaptive/MessageView.tsx b/packages/gitbook/src/components/Adaptive/MessageView.tsx
new file mode 100644
index 000000000..2d707360e
--- /dev/null
+++ b/packages/gitbook/src/components/Adaptive/MessageView.tsx
@@ -0,0 +1,31 @@
+import type { AIMessage } from '@gitbook/api';
+import { DocumentView } from '../DocumentView';
+
+/**
+ * Render a message from the API backend.
+ */
+export function MessageView(props: {
+ message: AIMessage;
+}) {
+ const { message } = props;
+
+ return (
+
+ {message.steps.map((step, index) => {
+ return (
+
+
+
+ );
+ })}
+
+ );
+}
diff --git a/packages/gitbook/src/components/Adaptive/server-actions/api.ts b/packages/gitbook/src/components/Adaptive/server-actions/api.ts
deleted file mode 100644
index 1ea43e8a5..000000000
--- a/packages/gitbook/src/components/Adaptive/server-actions/api.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-'use server';
-import {
- type AIMessageInput,
- AIModel,
- type AIStreamResponse,
- type AIToolCapabilities,
-} from '@gitbook/api';
-import type { GitBookBaseContext } from '@v2/lib/context';
-import { EventIterator } from 'event-iterator';
-import type { MaybePromise } from 'p-map';
-import * as partialJson from 'partial-json';
-import type { DeepPartial } from 'ts-essentials';
-import type { z } from 'zod';
-import { zodToJsonSchema } from 'zod-to-json-schema';
-
-/**
- * Get the latest value from a stream and the response id.
- */
-export async function generate(
- promise: MaybePromise<{
- stream: EventIterator;
- response: Promise<{ responseId: string }>;
- }>
-) {
- const input = await promise;
- let value: T | undefined;
-
- for await (const event of input.stream) {
- value = event;
- }
-
- const { responseId } = await input.response;
- return {
- responseId,
- value,
- };
-}
-
-/**
- * Stream the generation of an object using the AI.
- */
-export async function streamGenerateObject(
- context: GitBookBaseContext,
- {
- organizationId,
- siteId,
- }: {
- organizationId: string;
- siteId: string;
- },
- {
- schema,
- messages,
- model = AIModel.Fast,
- }: {
- schema: z.ZodSchema;
- messages: AIMessageInput[];
- model?: AIModel;
- tools?: AIToolCapabilities;
- previousResponseId?: string;
- }
-) {
- const rawStream = context.dataFetcher.streamAIResponse({
- organizationId,
- siteId,
- input: messages,
- output: {
- type: 'object',
- schema: zodToJsonSchema(schema),
- },
- model,
- });
-
- let json = '';
- return parseResponse>(rawStream, (event) => {
- if (event.type === 'response_object') {
- json += event.jsonChunk;
-
- const parsed = partialJson.parse(json, partialJson.ALL);
- return parsed;
- }
- });
-}
-
-/**
- * Parse a stream from the API to extract the responseId.
- */
-function parseResponse(
- responseStream: EventIterator,
- parse: (response: AIStreamResponse) => T | undefined
-): {
- stream: EventIterator;
- response: Promise<{ responseId: string }>;
-} {
- let resolveResponse: (value: { responseId: string }) => void;
- const response = new Promise<{ responseId: string }>((resolve) => {
- resolveResponse = resolve;
- });
-
- const stream = new EventIterator((queue) => {
- (async () => {
- let foundResponse = false;
-
- for await (const event of responseStream) {
- if (event.type === 'response_finish') {
- foundResponse = true;
- resolveResponse({ responseId: event.responseId });
- } else {
- const parsed = parse(event);
- if (parsed !== undefined) {
- queue.push(parsed);
- }
- }
- }
-
- if (!foundResponse) {
- throw new Error('No response found');
- }
- })().then(
- () => {
- queue.stop();
- },
- (error) => {
- queue.fail(error);
- }
- );
- });
-
- return { stream, response };
-}
diff --git a/packages/gitbook/src/components/Adaptive/server-actions/api.tsx b/packages/gitbook/src/components/Adaptive/server-actions/api.tsx
new file mode 100644
index 000000000..81b56d069
--- /dev/null
+++ b/packages/gitbook/src/components/Adaptive/server-actions/api.tsx
@@ -0,0 +1,214 @@
+'use server';
+import {
+ type AIMessage,
+ AIMessageRole,
+ type AIMessageStep,
+ type AIStreamResponse,
+} from '@gitbook/api';
+import type { GitBookBaseContext } from '@v2/lib/context';
+import type { GitBookDataFetcher } from '@v2/lib/data';
+import { EventIterator } from 'event-iterator';
+import type { MaybePromise } from 'p-map';
+import * as partialJson from 'partial-json';
+import type { DeepPartial } from 'ts-essentials';
+import type { z } from 'zod';
+import { zodToJsonSchema } from 'zod-to-json-schema';
+import { MessageView } from '../MessageView';
+
+/**
+ * Get the latest value from a stream and the response id.
+ */
+export async function generate(
+ promise: MaybePromise<{
+ stream: EventIterator;
+ response: Promise<{ responseId: string }>;
+ }>
+) {
+ const input = await promise;
+ let value: T | undefined;
+
+ for await (const event of input.stream) {
+ value = event;
+ }
+
+ const { responseId } = await input.response;
+ return {
+ responseId,
+ value,
+ };
+}
+
+/**
+ * Stream the generation of an object using the AI.
+ */
+export async function streamGenerateObject(
+ context: GitBookBaseContext,
+ {
+ schema,
+ ...input
+ }: Omit[0], 'output'> & {
+ schema: z.ZodSchema;
+ }
+) {
+ const rawStream = context.dataFetcher.streamAIResponse({
+ ...input,
+ output: {
+ type: 'object',
+ schema: zodToJsonSchema(schema),
+ },
+ });
+
+ let json = '';
+ return parseResponse>(rawStream, (event) => {
+ if (event.type === 'response_object') {
+ json += event.jsonChunk;
+
+ const parsed = partialJson.parse(json, partialJson.ALL);
+ return parsed;
+ }
+ });
+}
+
+/**
+ * Stream the generation of a document.
+ */
+export async function streamGenerateDocument(
+ context: GitBookBaseContext,
+ input: Omit[0], 'output'>
+) {
+ const rawStream = context.dataFetcher.streamAIResponse({
+ ...input,
+ output: {
+ type: 'document',
+ },
+ });
+
+ const message: AIMessage = {
+ id: '',
+ role: AIMessageRole.Assistant,
+ steps: [],
+ };
+
+ const updateProcessingMessageStep = (
+ stepIndex: number,
+ callback: (step: AIMessageStep) => void
+ ) => {
+ if (stepIndex > message.steps.length) {
+ throw new Error(
+ `Step index out of bounds ${stepIndex} (${message.steps.length} steps)`
+ );
+ }
+
+ if (message.steps[stepIndex]) {
+ message.steps = [...message.steps];
+ message.steps[stepIndex] = { ...message.steps[stepIndex] };
+ callback(message.steps[stepIndex]);
+ } else {
+ message.steps = [
+ ...message.steps,
+ {
+ content: {
+ object: 'document',
+ data: {},
+ nodes: [],
+ },
+ },
+ ];
+ callback(message.steps[stepIndex]);
+ }
+ };
+
+ return parseResponse(rawStream, (event) => {
+ switch (event.type) {
+ /**
+ * The agent is processing a tool call in a new message.
+ */
+ case 'response_tool_call': {
+ updateProcessingMessageStep(event.stepIndex, (step) => {
+ step.toolCalls ??= [];
+ step.toolCalls.push(event.toolCall);
+ });
+ break;
+ }
+
+ /**
+ * The agent is writing the content of a new message.
+ */
+ case 'response_reasoning':
+ case 'response_document': {
+ updateProcessingMessageStep(event.stepIndex, (step) => {
+ const container = event.type === 'response_reasoning' ? 'reasoning' : 'content';
+
+ step[container] ??= {
+ object: 'document',
+ data: {},
+ nodes: [],
+ };
+ step[container] = {
+ ...step[container],
+ nodes: [...step[container].nodes],
+ };
+ if (event.operation === 'insert') {
+ step[container].nodes.push(...event.blocks);
+ } else {
+ step[container].nodes.splice(
+ -event.blocks.length,
+ event.blocks.length,
+ ...event.blocks
+ );
+ }
+ });
+ break;
+ }
+ }
+
+ return ;
+ });
+}
+
+/**
+ * Parse a stream from the API to extract the responseId.
+ */
+function parseResponse(
+ responseStream: EventIterator,
+ parse: (response: AIStreamResponse) => T | undefined
+): {
+ stream: EventIterator;
+ response: Promise<{ responseId: string }>;
+} {
+ let resolveResponse: (value: { responseId: string }) => void;
+ const response = new Promise<{ responseId: string }>((resolve) => {
+ resolveResponse = resolve;
+ });
+
+ const stream = new EventIterator((queue) => {
+ (async () => {
+ let foundResponse = false;
+
+ for await (const event of responseStream) {
+ if (event.type === 'response_finish') {
+ foundResponse = true;
+ resolveResponse({ responseId: event.responseId });
+ } else {
+ const parsed = parse(event);
+ if (parsed !== undefined) {
+ queue.push(parsed);
+ }
+ }
+ }
+
+ if (!foundResponse) {
+ throw new Error('No response found');
+ }
+ })().then(
+ () => {
+ queue.stop();
+ },
+ (error) => {
+ queue.fail(error);
+ }
+ );
+ });
+
+ return { stream, response };
+}
diff --git a/packages/gitbook/src/components/Adaptive/server-actions/index.ts b/packages/gitbook/src/components/Adaptive/server-actions/index.ts
index 664e869e2..3e9f6d001 100644
--- a/packages/gitbook/src/components/Adaptive/server-actions/index.ts
+++ b/packages/gitbook/src/components/Adaptive/server-actions/index.ts
@@ -1 +1,2 @@
export * from './streamLinkPageSummary';
+export * from './api';
diff --git a/packages/gitbook/src/components/Adaptive/server-actions/streamAskAIAnswer.ts b/packages/gitbook/src/components/Adaptive/server-actions/streamAskAIAnswer.ts
deleted file mode 100644
index 88abfe019..000000000
--- a/packages/gitbook/src/components/Adaptive/server-actions/streamAskAIAnswer.ts
+++ /dev/null
@@ -1,166 +0,0 @@
-'use server';
-import { filterOutNullable } from '@/lib/typescript';
-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 summary of a page, in the context of another page
- */
-export async function* streamLinkPageSummary({
- currentSpaceId,
- currentPageId,
- targetSpaceId,
- targetPageId,
- linkPreview,
- linkTitle,
- visitedPages,
-}: {
- currentSpaceId: string;
- currentPageId: string;
- currentPageTitle: string;
- targetSpaceId: string;
- targetPageId: string;
- linkPreview?: string;
- linkTitle?: string;
- visitedPages?: Array<{ spaceId: 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({
- highlight: z
- .string()
- .describe('The reason why the user should read the target page.'),
- // questions: z.array(z.string().describe('The questions to sea')).max(3),
- }),
- messages: [
- {
- role: AIMessageRole.Developer,
- content: `# 1. Role
-You are a contextual fact extractor. Your job is to find the exact fact from the linked page that directly answers the implied question in the current paragraph.
-
-# 2. Task
-Extract a contextually-relevant fact that:
-- Directly answers the specific need or question implied by the link's placement
-- States a capability, limitation, or specification from the target page
-- Connects precisely to the user's current paragraph or sentence
-- Completes the user's understanding based on what they're currently reading
-
-# 3. Instructions
-1. First, identify the exact need, question, or gap in the current paragraph where the link appears
-2. Find the specific fact in the target page that addresses this exact contextual need
-3. Ensure the fact relates directly to the context of the paragraph containing the link
-4. Avoid ALL instructional language including words like "use", "click", "select", "create"
-5. Keep it under 30 words, factual and declarative about what EXISTS or IS TRUE`,
- },
- {
- role: AIMessageRole.Developer,
- content: `# 4. Current page
-The content of the current page is:`,
- attachments: [
- {
- type: 'page' as const,
- spaceId: currentSpaceId,
- pageId: currentPageId,
- },
- ],
- },
- ...(visitedPages
- ? [
- {
- role: AIMessageRole.Developer,
- content: '# 5. Previous pages',
- },
- ...visitedPages.map(({ spaceId, pageId }) => ({
- role: AIMessageRole.Developer,
- content: `## Page ${pageId}`,
- attachments: [
- {
- type: 'page' as const,
- spaceId,
- pageId,
- },
- ],
- })),
- ]
- : []),
- {
- role: AIMessageRole.Developer,
- content: `# 6. Target page
-The content of the target page is:`,
- attachments: [
- {
- type: 'page' as const,
- spaceId: targetSpaceId,
- pageId: targetPageId,
- },
- ],
- },
- {
- role: AIMessageRole.Developer,
- content: `# 7. Link preview
-The content of the link preview is:
-> ${linkPreview}
-> Page ID: ${targetPageId}`,
- },
- {
- role: AIMessageRole.Developer,
- content: `# 8. Guidelines & Examples
-ALWAYS:
-- ALWAYS choose facts that directly fulfill the contextual need where the link appears
-- ALWAYS connect target page information specifically to the current paragraph context
-- ALWAYS focus on the gap in knowledge that the link is meant to fill
-- ALWAYS consider user's navigation history to ensure contextual continuity
-- ALWAYS use action verbs like "click", "select", "use", "create", "enable"
-
-NEVER:
-- NEVER include ANY unspecifc language like "learn", "how to", "discover", etc. State the fact directly.
-- NEVER select general facts unrelated to the specific link context
-- NEVER ignore the specific context where the link appears
-- NEVER repeat the same fact in different words
-
-## Examples
-Current paragraph: "When organizing content, headings are limited to 3 levels. For more advanced editing, you can use (multiple select)[/multiple-select] to move multiple blocks at once."
-Preview: "Multiple Select: Select multiple content blocks at once."
-✓ "Shift selects content between two points, useful for reorganizing your current heading structure."
-✗ "Shift and Ctrl/Cmd keys are the modifiers for selecting multiple blocks."
-
-Current paragraph: "Most changes can be published directly, but for major revisions, if you want others to review changes before publishing, create a (change request)[/change-requests]."
-Preview: "Change Requests: Collaborative content editing workflow."
-✓ "Each reviewer's approval is tracked separately, with specific change highlighting for your major revisions."
-✗ "Each reviewer receives an email notification and can approve or request changes."
-
-Current paragraph: "Your team mentioned issues with conflicting edits. Need to collaborate in real-time? You can use (live edit mode)[/live-edit]."
-Preview: "Live Edit: Real-time collaborative editing."
-✓ "Teams with GitHub repositories (like yours) cannot use this feature due to sync limitations."
-✗ "Incompatible with GitHub/GitLab sync and requires specific visibility settings."`,
- },
- {
- role: AIMessageRole.User,
- content: `I'm considering reading the link titled "${linkTitle}" pointing to page ${targetPageId}. Why should I read it? Relate it to the paragraph I'm currently reading.`,
- },
- ].filter(filterOutNullable),
- }
- );
-
- for await (const value of stream) {
- const highlight = value.highlight;
- if (!highlight) {
- continue;
- }
-
- yield highlight;
- }
-}
diff --git a/packages/gitbook/src/components/Adaptive/server-actions/streamLinkPageSummary.ts b/packages/gitbook/src/components/Adaptive/server-actions/streamLinkPageSummary.ts
index 88abfe019..65078e480 100644
--- a/packages/gitbook/src/components/Adaptive/server-actions/streamLinkPageSummary.ts
+++ b/packages/gitbook/src/components/Adaptive/server-actions/streamLinkPageSummary.ts
@@ -2,7 +2,7 @@
import { filterOutNullable } from '@/lib/typescript';
import { getV1BaseContext } from '@/lib/v1';
import { isV2 } from '@/lib/v2';
-import { AIMessageRole } from '@gitbook/api';
+import { AIMessageRole, AIModel } from '@gitbook/api';
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
import { getServerActionBaseContext } from '@v2/lib/server-actions';
import { z } from 'zod';
@@ -32,23 +32,18 @@ export async function* streamLinkPageSummary({
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({
- highlight: z
- .string()
- .describe('The reason why the user should read the target page.'),
- // questions: z.array(z.string().describe('The questions to sea')).max(3),
- }),
- messages: [
- {
- role: AIMessageRole.Developer,
- content: `# 1. Role
+ const { stream } = await streamGenerateObject(baseContext, {
+ organizationId: siteURLData.organization,
+ siteId: siteURLData.site,
+ model: AIModel.Fast,
+ schema: z.object({
+ highlight: z.string().describe('The reason why the user should read the target page.'),
+ // questions: z.array(z.string().describe('The questions to sea')).max(3),
+ }),
+ input: [
+ {
+ role: AIMessageRole.Developer,
+ content: `# 1. Role
You are a contextual fact extractor. Your job is to find the exact fact from the linked page that directly answers the implied question in the current paragraph.
# 2. Task
@@ -64,60 +59,60 @@ Extract a contextually-relevant fact that:
3. Ensure the fact relates directly to the context of the paragraph containing the link
4. Avoid ALL instructional language including words like "use", "click", "select", "create"
5. Keep it under 30 words, factual and declarative about what EXISTS or IS TRUE`,
- },
- {
- role: AIMessageRole.Developer,
- content: `# 4. Current page
+ },
+ {
+ role: AIMessageRole.Developer,
+ content: `# 4. Current page
The content of the current page is:`,
- attachments: [
- {
- type: 'page' as const,
- spaceId: currentSpaceId,
- pageId: currentPageId,
- },
- ],
- },
- ...(visitedPages
- ? [
- {
- role: AIMessageRole.Developer,
- content: '# 5. Previous pages',
- },
- ...visitedPages.map(({ spaceId, pageId }) => ({
- role: AIMessageRole.Developer,
- content: `## Page ${pageId}`,
- attachments: [
- {
- type: 'page' as const,
- spaceId,
- pageId,
- },
- ],
- })),
- ]
- : []),
- {
- role: AIMessageRole.Developer,
- content: `# 6. Target page
+ attachments: [
+ {
+ type: 'page' as const,
+ spaceId: currentSpaceId,
+ pageId: currentPageId,
+ },
+ ],
+ },
+ ...(visitedPages
+ ? [
+ {
+ role: AIMessageRole.Developer,
+ content: '# 5. Previous pages',
+ },
+ ...visitedPages.map(({ spaceId, pageId }) => ({
+ role: AIMessageRole.Developer,
+ content: `## Page ${pageId}`,
+ attachments: [
+ {
+ type: 'page' as const,
+ spaceId,
+ pageId,
+ },
+ ],
+ })),
+ ]
+ : []),
+ {
+ role: AIMessageRole.Developer,
+ content: `# 6. Target page
The content of the target page is:`,
- attachments: [
- {
- type: 'page' as const,
- spaceId: targetSpaceId,
- pageId: targetPageId,
- },
- ],
- },
- {
- role: AIMessageRole.Developer,
- content: `# 7. Link preview
+ attachments: [
+ {
+ type: 'page' as const,
+ spaceId: targetSpaceId,
+ pageId: targetPageId,
+ },
+ ],
+ },
+ {
+ role: AIMessageRole.Developer,
+ content: `# 7. Link preview
The content of the link preview is:
> ${linkPreview}
> Page ID: ${targetPageId}`,
- },
- {
- role: AIMessageRole.Developer,
- content: `# 8. Guidelines & Examples
+ },
+ {
+ role: AIMessageRole.Developer,
+ content: `# 8. Guidelines & Examples
ALWAYS:
- ALWAYS choose facts that directly fulfill the contextual need where the link appears
- ALWAYS connect target page information specifically to the current paragraph context
@@ -146,14 +141,13 @@ Current paragraph: "Your team mentioned issues with conflicting edits. Need to c
Preview: "Live Edit: Real-time collaborative editing."
✓ "Teams with GitHub repositories (like yours) cannot use this feature due to sync limitations."
✗ "Incompatible with GitHub/GitLab sync and requires specific visibility settings."`,
- },
- {
- role: AIMessageRole.User,
- content: `I'm considering reading the link titled "${linkTitle}" pointing to page ${targetPageId}. Why should I read it? Relate it to the paragraph I'm currently reading.`,
- },
- ].filter(filterOutNullable),
- }
- );
+ },
+ {
+ role: AIMessageRole.User,
+ content: `I'm considering reading the link titled "${linkTitle}" pointing to page ${targetPageId}. Why should I read it? Relate it to the paragraph I'm currently reading.`,
+ },
+ ].filter(filterOutNullable),
+ });
for await (const value of stream) {
const highlight = value.highlight;
diff --git a/packages/gitbook/src/components/Search/SearchChat.tsx b/packages/gitbook/src/components/Search/SearchChat.tsx
index 6289dcfea..868c66a73 100644
--- a/packages/gitbook/src/components/Search/SearchChat.tsx
+++ b/packages/gitbook/src/components/Search/SearchChat.tsx
@@ -226,7 +226,7 @@ function useAIStream({
// Summary hook
function useSummary(visitedPages: any[]) {
- const [summary, setSummary] = useState('');
+ const [summary, setSummary] = useState(undefined);
const [summaryResponseId, setSummaryResponseId] = useState(undefined);
useEffect(() => {
@@ -236,19 +236,16 @@ function useSummary(visitedPages: any[]) {
try {
const stream = await streamAISearchSummary({ visitedPages });
- for await (const rawData of stream) {
+ for await (const data of stream) {
if (cancelled) break;
- if (!rawData) continue;
-
- // Use type assertion
- const data = rawData as any;
+ if (!data) continue;
if (data.responseId) {
setSummaryResponseId(String(data.responseId));
}
- if (data.summary) {
- setSummary(String(data.summary));
+ if (data.output) {
+ setSummary(data.output);
}
}
} catch (error) {
diff --git a/packages/gitbook/src/components/Search/server-actions.tsx b/packages/gitbook/src/components/Search/server-actions.tsx
index 822db47ba..35210ffe5 100644
--- a/packages/gitbook/src/components/Search/server-actions.tsx
+++ b/packages/gitbook/src/components/Search/server-actions.tsx
@@ -6,6 +6,7 @@ import { filterOutNullable } from '@/lib/typescript';
import { getV1BaseContext } from '@/lib/v1';
import {
AIMessageRole,
+ AIModel,
type RevisionPage,
type SearchAIAnswer,
type SearchAIRecommendedQuestionStream,
@@ -26,7 +27,7 @@ import type { IconName } from '@gitbook/icons';
import { throwIfDataError } from '@v2/lib/data';
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
import { z } from 'zod';
-import { streamGenerateObject } from '../Adaptive/server-actions/api';
+import { streamGenerateDocument, streamGenerateObject } from '../Adaptive/server-actions';
import { DocumentView } from '../DocumentView';
export type OrderedComputedResult = ComputedPageResult | ComputedSectionResult;
@@ -425,56 +426,30 @@ export async function* streamAISearchSummary({
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
const siteURLData = await getSiteURLDataFromMiddleware();
- const { stream, response } = await streamGenerateObject(
- baseContext,
- {
- organizationId: siteURLData.organization,
- siteId: siteURLData.site,
- },
- {
- schema: z.object({
- summary: z
- .string()
- .describe(
- 'A summary of the most important information the user has learned from the provided context.'
- ),
- }),
- messages: [
- {
- role: AIMessageRole.Developer,
- content:
- 'Summarise the most important information the user has learned from the provided context. Be concise and focus on facts. Do not add commentary, adjectives or other empty descriptors.',
- attachments: visitedPages.map(({ spaceId, pageId }) => ({
- type: 'page' as const,
- spaceId,
- pageId,
- })),
- },
- ].filter(filterOutNullable),
- }
- );
+ const { response, stream } = await streamGenerateDocument(baseContext, {
+ organizationId: siteURLData.organization,
+ siteId: siteURLData.site,
+ model: AIModel.Fast,
+ input: [
+ {
+ role: AIMessageRole.Developer,
+ content:
+ 'Summarise the most important information the user has learned from the provided context. Be concise and focus on facts. Do not add commentary, adjectives or other empty descriptors.',
+ attachments: visitedPages.map(({ spaceId, pageId }) => ({
+ type: 'page' as const,
+ spaceId,
+ pageId,
+ })),
+ },
+ ].filter(filterOutNullable),
+ });
- // Get the responseId asynchronously in the background
- let responseId: string | null = null;
- const responseIdPromise = response
- .then((r) => {
- responseId = r.responseId;
- })
- .catch((error) => {
- console.error('Error getting responseId:', error);
- });
-
- for await (const value of stream) {
- const summary = value.summary;
- if (!summary) {
- continue;
- }
-
- yield { summary };
+ for await (const output of stream) {
+ yield { output };
}
// Wait for the responseId to be available and yield one final time
- await responseIdPromise;
+ const responseId = await response;
yield { responseId };
}
diff --git a/packages/react-contentkit/package.json b/packages/react-contentkit/package.json
index 09e6cc8e7..521485c1c 100644
--- a/packages/react-contentkit/package.json
+++ b/packages/react-contentkit/package.json
@@ -10,7 +10,7 @@
},
"dependencies": {
"classnames": "^2.5.1",
- "@gitbook/api": "^0.115.0",
+ "@gitbook/api": "^0.117.1",
"@gitbook/icons": "workspace:*"
},
"peerDependencies": {