Skip to content

Commit 0ef6475

Browse files
authored
Hooks and APIs for AI v2 (#3385)
1 parent b403962 commit 0ef6475

32 files changed

+1428
-423
lines changed

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

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,6 @@ export function createDataFetcher(
169169
getUserById(userId) {
170170
return trace('getUserById', () => getUserById(input, { userId }));
171171
},
172-
173-
streamAIResponse(params) {
174-
return streamAIResponse(input, params);
175-
},
176172
};
177173
}
178174

@@ -657,29 +653,6 @@ const renderIntegrationUi = cache(
657653
}
658654
);
659655

660-
async function* streamAIResponse(
661-
input: DataFetcherInput,
662-
params: Parameters<GitBookDataFetcher['streamAIResponse']>[0]
663-
) {
664-
const api = apiClient(input);
665-
const res = await api.orgs.streamAiResponseInSite(
666-
params.organizationId,
667-
params.siteId,
668-
{
669-
input: params.input,
670-
output: params.output,
671-
model: params.model,
672-
},
673-
{
674-
...noCacheFetchOptions,
675-
}
676-
);
677-
678-
for await (const event of res) {
679-
yield event;
680-
}
681-
}
682-
683656
/**
684657
* Create a new API client.
685658
*/

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

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,4 @@ export interface GitBookDataFetcher {
160160
integrationName: string;
161161
request: api.RenderIntegrationUI;
162162
}): Promise<DataFetcherResponse<api.ContentKitRenderOutput>>;
163-
164-
/**
165-
* Stream an AI response.
166-
*/
167-
streamAIResponse(params: {
168-
organizationId: string;
169-
siteId: string;
170-
input: api.AIMessageInput[];
171-
output: api.AIOutputFormat;
172-
model: api.AIModel;
173-
}): AsyncGenerator<api.AIStreamResponse, void, unknown>;
174163
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './useAIPage';
2+
export * from './useAIChat';
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { AIMessage } from '@gitbook/api';
2+
import type { GitBookSiteContext } from '@v2/lib/context';
3+
import { DocumentView } from '../../DocumentView';
4+
import { AIToolCallsSummary } from './AIToolCallsSummary';
5+
import type { RenderAIMessageOptions } from './types';
6+
7+
/**
8+
* Render a message from the API backend.
9+
*/
10+
export function AIMessageView(
11+
props: RenderAIMessageOptions & {
12+
message: AIMessage;
13+
context: GitBookSiteContext;
14+
}
15+
) {
16+
const { message, context, renderToolCalls = true } = props;
17+
18+
return (
19+
<div className="flex flex-col gap-2">
20+
{message.steps.map((step, index) => {
21+
return (
22+
<div key={index} className="flex flex-col gap-2">
23+
<DocumentView
24+
document={step.content}
25+
context={{
26+
mode: 'default',
27+
contentContext: undefined,
28+
wrapBlocksInSuspense: false,
29+
}}
30+
style={['space-y-5']}
31+
/>
32+
{renderToolCalls && step.toolCalls && step.toolCalls.length > 0 ? (
33+
<AIToolCallsSummary toolCalls={step.toolCalls} context={context} />
34+
) : null}
35+
</div>
36+
);
37+
})}
38+
</div>
39+
);
40+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { Link } from '@/components/primitives';
2+
import { resolveContentRef } from '@/lib/references';
3+
import type { AIToolCall, ContentRef } from '@gitbook/api';
4+
import { Icon, type IconName } from '@gitbook/icons';
5+
import type { GitBookSiteContext } from '@v2/lib/context';
6+
import type * as React from 'react';
7+
8+
/**
9+
* Display the tool calls in a message or step.
10+
*/
11+
export function AIToolCallsSummary(props: {
12+
toolCalls: AIToolCall[];
13+
context: GitBookSiteContext;
14+
}) {
15+
const { toolCalls, context } = props;
16+
17+
return (
18+
<div className="flex flex-col gap-1">
19+
{toolCalls.map((toolCall, index) => (
20+
<ToolCallSummary key={index} toolCall={toolCall} context={context} />
21+
))}
22+
</div>
23+
);
24+
}
25+
26+
function ToolCallSummary(props: {
27+
toolCall: AIToolCall;
28+
context: GitBookSiteContext;
29+
}) {
30+
const { toolCall, context } = props;
31+
32+
return (
33+
<p className="text-slate-700 text-sm">
34+
<Icon
35+
icon={getIconForToolCall(toolCall)}
36+
className="mr-1 inline-block size-3 text-slate-300"
37+
/>
38+
{getDescriptionForToolCall(toolCall, context)}
39+
</p>
40+
);
41+
}
42+
43+
function getDescriptionForToolCall(
44+
toolCall: AIToolCall,
45+
context: GitBookSiteContext
46+
): React.ReactNode {
47+
switch (toolCall.tool) {
48+
case 'getPageContent':
49+
return (
50+
<>
51+
Read page{' '}
52+
<ContentRefLink
53+
contentRef={{
54+
kind: 'page',
55+
page: toolCall.page.id,
56+
space: toolCall.spaceId,
57+
}}
58+
context={context}
59+
fallback={toolCall.page.title}
60+
/>
61+
<OtherSpaceLink spaceId={toolCall.spaceId} context={context} />
62+
</>
63+
);
64+
case 'search':
65+
// TODO: Show in a popover the results using the list `toolCall.results`.
66+
return (
67+
<>
68+
Searched <strong>{toolCall.query}</strong>
69+
</>
70+
);
71+
case 'getPages':
72+
return (
73+
<>
74+
Listed the pages
75+
<OtherSpaceLink spaceId={toolCall.spaceId} context={context} />
76+
</>
77+
);
78+
default:
79+
return <>{toolCall.tool}</>;
80+
}
81+
}
82+
83+
function getIconForToolCall(toolCall: AIToolCall): IconName {
84+
switch (toolCall.tool) {
85+
case 'getPageContent':
86+
return 'memo';
87+
case 'search':
88+
return 'magnifying-glass';
89+
case 'getPages':
90+
return 'files';
91+
default:
92+
return 'hammer';
93+
}
94+
}
95+
96+
/**
97+
* Link to a space that is not the current space.
98+
*/
99+
function OtherSpaceLink(props: {
100+
spaceId: string;
101+
context: GitBookSiteContext;
102+
prefix?: React.ReactNode;
103+
}) {
104+
const { spaceId, prefix = ' in ', context } = props;
105+
106+
if (context.space.id === spaceId) {
107+
return null;
108+
}
109+
110+
return (
111+
<>
112+
{prefix}
113+
<ContentRefLink
114+
contentRef={{
115+
kind: 'space',
116+
space: spaceId,
117+
}}
118+
context={context}
119+
/>
120+
</>
121+
);
122+
}
123+
124+
async function ContentRefLink(props: {
125+
contentRef: ContentRef;
126+
context: GitBookSiteContext;
127+
fallback?: React.ReactNode;
128+
}) {
129+
const { contentRef, context, fallback } = props;
130+
131+
const resolved = await resolveContentRef(contentRef, context);
132+
133+
if (!resolved) {
134+
return <span>{fallback}</span>;
135+
}
136+
137+
return (
138+
<Link href={resolved.href} className="text-inherit underline decoration-dashed">
139+
{resolved.text}
140+
</Link>
141+
);
142+
}

0 commit comments

Comments
 (0)