Skip to content

Commit 34eed44

Browse files
committed
Create Chat component
1 parent 955baef commit 34eed44

File tree

4 files changed

+356
-12
lines changed

4 files changed

+356
-12
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
'use server';
2+
import { filterOutNullable } from '@/lib/typescript';
3+
import { getV1BaseContext } from '@/lib/v1';
4+
import { isV2 } from '@/lib/v2';
5+
import { AIMessageRole } from '@gitbook/api';
6+
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
7+
import { getServerActionBaseContext } from '@v2/lib/server-actions';
8+
import { z } from 'zod';
9+
import { streamGenerateObject } from './api';
10+
11+
/**
12+
* Get a summary of a page, in the context of another page
13+
*/
14+
export async function* streamLinkPageSummary({
15+
currentSpaceId,
16+
currentPageId,
17+
targetSpaceId,
18+
targetPageId,
19+
linkPreview,
20+
linkTitle,
21+
visitedPages,
22+
}: {
23+
currentSpaceId: string;
24+
currentPageId: string;
25+
currentPageTitle: string;
26+
targetSpaceId: string;
27+
targetPageId: string;
28+
linkPreview?: string;
29+
linkTitle?: string;
30+
visitedPages?: Array<{ spaceId: string; pageId: string }>;
31+
}) {
32+
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
33+
const siteURLData = await getSiteURLDataFromMiddleware();
34+
35+
const { stream } = await streamGenerateObject(
36+
baseContext,
37+
{
38+
organizationId: siteURLData.organization,
39+
siteId: siteURLData.site,
40+
},
41+
{
42+
schema: z.object({
43+
highlight: z
44+
.string()
45+
.describe('The reason why the user should read the target page.'),
46+
// questions: z.array(z.string().describe('The questions to sea')).max(3),
47+
}),
48+
messages: [
49+
{
50+
role: AIMessageRole.Developer,
51+
content: `# 1. Role
52+
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.
53+
54+
# 2. Task
55+
Extract a contextually-relevant fact that:
56+
- Directly answers the specific need or question implied by the link's placement
57+
- States a capability, limitation, or specification from the target page
58+
- Connects precisely to the user's current paragraph or sentence
59+
- Completes the user's understanding based on what they're currently reading
60+
61+
# 3. Instructions
62+
1. First, identify the exact need, question, or gap in the current paragraph where the link appears
63+
2. Find the specific fact in the target page that addresses this exact contextual need
64+
3. Ensure the fact relates directly to the context of the paragraph containing the link
65+
4. Avoid ALL instructional language including words like "use", "click", "select", "create"
66+
5. Keep it under 30 words, factual and declarative about what EXISTS or IS TRUE`,
67+
},
68+
{
69+
role: AIMessageRole.Developer,
70+
content: `# 4. Current page
71+
The content of the current page is:`,
72+
attachments: [
73+
{
74+
type: 'page' as const,
75+
spaceId: currentSpaceId,
76+
pageId: currentPageId,
77+
},
78+
],
79+
},
80+
...(visitedPages
81+
? [
82+
{
83+
role: AIMessageRole.Developer,
84+
content: '# 5. Previous pages',
85+
},
86+
...visitedPages.map(({ spaceId, pageId }) => ({
87+
role: AIMessageRole.Developer,
88+
content: `## Page ${pageId}`,
89+
attachments: [
90+
{
91+
type: 'page' as const,
92+
spaceId,
93+
pageId,
94+
},
95+
],
96+
})),
97+
]
98+
: []),
99+
{
100+
role: AIMessageRole.Developer,
101+
content: `# 6. Target page
102+
The content of the target page is:`,
103+
attachments: [
104+
{
105+
type: 'page' as const,
106+
spaceId: targetSpaceId,
107+
pageId: targetPageId,
108+
},
109+
],
110+
},
111+
{
112+
role: AIMessageRole.Developer,
113+
content: `# 7. Link preview
114+
The content of the link preview is:
115+
> ${linkPreview}
116+
> Page ID: ${targetPageId}`,
117+
},
118+
{
119+
role: AIMessageRole.Developer,
120+
content: `# 8. Guidelines & Examples
121+
ALWAYS:
122+
- ALWAYS choose facts that directly fulfill the contextual need where the link appears
123+
- ALWAYS connect target page information specifically to the current paragraph context
124+
- ALWAYS focus on the gap in knowledge that the link is meant to fill
125+
- ALWAYS consider user's navigation history to ensure contextual continuity
126+
- ALWAYS use action verbs like "click", "select", "use", "create", "enable"
127+
128+
NEVER:
129+
- NEVER include ANY unspecifc language like "learn", "how to", "discover", etc. State the fact directly.
130+
- NEVER select general facts unrelated to the specific link context
131+
- NEVER ignore the specific context where the link appears
132+
- NEVER repeat the same fact in different words
133+
134+
## Examples
135+
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."
136+
Preview: "Multiple Select: Select multiple content blocks at once."
137+
✓ "Shift selects content between two points, useful for reorganizing your current heading structure."
138+
✗ "Shift and Ctrl/Cmd keys are the modifiers for selecting multiple blocks."
139+
140+
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]."
141+
Preview: "Change Requests: Collaborative content editing workflow."
142+
✓ "Each reviewer's approval is tracked separately, with specific change highlighting for your major revisions."
143+
✗ "Each reviewer receives an email notification and can approve or request changes."
144+
145+
Current paragraph: "Your team mentioned issues with conflicting edits. Need to collaborate in real-time? You can use (live edit mode)[/live-edit]."
146+
Preview: "Live Edit: Real-time collaborative editing."
147+
✓ "Teams with GitHub repositories (like yours) cannot use this feature due to sync limitations."
148+
✗ "Incompatible with GitHub/GitLab sync and requires specific visibility settings."`,
149+
},
150+
{
151+
role: AIMessageRole.User,
152+
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.`,
153+
},
154+
].filter(filterOutNullable),
155+
}
156+
);
157+
158+
for await (const value of stream) {
159+
const highlight = value.highlight;
160+
if (!highlight) {
161+
continue;
162+
}
163+
164+
yield highlight;
165+
}
166+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use client';
2+
import { Icon } from '@gitbook/icons';
3+
import { motion } from 'framer-motion';
4+
import { useEffect, useState } from 'react';
5+
import { useVisitedPages } from '../Insights/useVisitedPages';
6+
import { streamAISearchSummary } from './server-actions';
7+
8+
export function SearchChat() {
9+
// const currentPage = usePageContext();
10+
// const language = useLanguage();
11+
const visitedPages = useVisitedPages((state) => state.pages);
12+
const [summary, setSummary] = useState('');
13+
const [responseId, setResponseId] = useState<string | null>(null);
14+
15+
useEffect(() => {
16+
let cancelled = false;
17+
18+
(async () => {
19+
const stream = await streamAISearchSummary({
20+
visitedPages,
21+
});
22+
23+
let generatedSummary = '';
24+
for await (const data of stream) {
25+
if (cancelled) return;
26+
27+
if ('responseId' in data && data.responseId !== undefined) {
28+
setResponseId(data.responseId);
29+
}
30+
31+
if ('summary' in data && data.summary !== undefined) {
32+
generatedSummary = data.summary;
33+
setSummary(generatedSummary);
34+
}
35+
}
36+
})();
37+
38+
return () => {
39+
cancelled = true;
40+
};
41+
}, [visitedPages]);
42+
43+
return (
44+
<motion.div layout="position" className="w-full">
45+
<h5 className="mb-1 flex items-center gap-1 font-semibold text-sm text-tint-subtle">
46+
<Icon icon="glasses-round" className="mt-0.5 size-4" /> Summary of what you've read
47+
</h5>
48+
49+
{summary ? (
50+
summary
51+
) : (
52+
<div key="loading" className="mt-2 flex flex-wrap gap-2">
53+
{[...Array(9)].map((_, index) => (
54+
<div
55+
key={index}
56+
className="h-4 animate-[fadeIn_0.5s_ease-in-out_both,pulse_2s_ease-in-out_infinite] rounded straight-corners:rounded-none bg-tint-active"
57+
style={{
58+
animationDelay: `${index * 0.1}s,${0.5 + index * 0.1}s`,
59+
width: `${((index % 5) + 1) * 15}%`,
60+
}}
61+
/>
62+
))}
63+
</div>
64+
)}
65+
</motion.div>
66+
);
67+
}

packages/gitbook/src/components/Search/SearchModal.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import { tcls } from '@/lib/tailwind';
99

1010
import { Button } from '../primitives/Button';
1111
import { LoadingPane } from '../primitives/LoadingPane';
12-
import { SearchAskAnswer } from './SearchAskAnswer';
1312
import { SearchAskProvider, useSearchAskState } from './SearchAskContext';
13+
import { SearchChat } from './SearchChat';
1414
import { SearchResults, type SearchResultsRef } from './SearchResults';
1515
import { SearchScopeToggle } from './SearchScopeToggle';
1616
import { type SearchState, type UpdateSearchState, useSearch } from './useSearch';
@@ -317,7 +317,7 @@ function SearchModalBody(
317317
key="chat"
318318
layout
319319
className={tcls(
320-
'-col-end-1 flex items-start gap-4 overflow-y-auto overflow-x-hidden border-tint-subtle bg-tint-subtle p-8 max-md:border-t md:row-start-2 md:border-l',
320+
'md:-col-end-1 flex items-start gap-4 overflow-y-auto overflow-x-hidden border-tint-subtle bg-tint-subtle p-8 max-md:border-t md:row-start-2 md:border-l',
321321
state.mode === 'chat' && 'md:col-start-1'
322322
)}
323323
initial={{ width: 0 }}
@@ -338,7 +338,7 @@ function SearchModalBody(
338338
}}
339339
/>
340340
) : null}
341-
<SearchAskAnswer query={normalizedQuery} />
341+
<SearchChat />
342342
</motion.div>
343343
) : null}
344344
</AnimatePresence>

0 commit comments

Comments
 (0)