Skip to content

New Search UI #3230

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/gitbook-v2/src/lib/data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,5 +189,7 @@ export interface GitBookDataFetcher {
input: api.AIMessageInput[];
output: api.AIOutputFormat;
model: api.AIModel;
tools?: api.AIToolCapabilities;
previousResponseId?: string;
}): AsyncGenerator<api.AIStreamResponse, void, unknown>;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
'use server';
import { type AIMessageInput, AIModel, type AIStreamResponse } from '@gitbook/api';
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';
Expand Down Expand Up @@ -51,6 +56,7 @@ export async function streamGenerateObject<T>(
schema: z.ZodSchema<T>;
messages: AIMessageInput[];
model?: AIModel;
tools?: AIToolCapabilities;
previousResponseId?: string;
}
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
'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;
}
}
2 changes: 1 addition & 1 deletion packages/gitbook/src/components/RootLayout/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

/* Light mode */
::-webkit-scrollbar {
@apply bg-tint-subtle;
@apply bg-tint-subtle z-50;
width: 8px;
height: 8px;
}
Expand Down
3 changes: 1 addition & 2 deletions packages/gitbook/src/components/Search/HighlightQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ export function HighlightQuery(props: {
'text-bold',
'bg-primary',
'text-contrast-primary',
'px-0.5',
'-mx-0.5',
'px-1',
'py-0.5',
'rounded',
'straight-corners:rounded-sm',
Expand Down
33 changes: 18 additions & 15 deletions packages/gitbook/src/components/Search/SearchAskAnswer.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
'use client';

import { Icon } from '@gitbook/icons';
import { readStreamableValue } from 'ai/rsc';
import React from 'react';

import { Loading } from '@/components/primitives';
import { useLanguage } from '@/intl/client';
import { t } from '@/intl/translate';
import type { TranslationLanguage } from '@/intl/translations';
import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons';
import { readStreamableValue } from 'ai/rsc';
import React from 'react';

import { motion } from 'framer-motion';
import { useTrackEvent } from '../Insights';
import { Link } from '../primitives';
import { useSearchAskContext } from './SearchAskContext';
import { type AskAnswerResult, type AskAnswerSource, streamAskQuestion } from './server-actions';
import { useSearch, useSearchLink } from './useSearch';

export type SearchAskState =
| {
type: 'answer';
Expand Down Expand Up @@ -88,13 +86,22 @@ export function SearchAskAnswer(props: { query: string }) {
}, [setAskState]);

const loading = (
<div className={tcls('w-full', 'flex', 'items-center', 'justify-center')}>
<Loading className={tcls('w-6', 'py-8', 'text-primary-subtle')} />
<div key="loading" className={tcls('flex', 'flex-wrap', 'gap-2')}>
{[...Array(9)].map((_, index) => (
<div
key={index}
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"
style={{
animationDelay: `${index * 0.1}s,${0.5 + index * 0.1}s`,
width: `${((index % 5) + 1) * 15}%`,
}}
/>
))}
</div>
);

return (
<div className={tcls('max-h-[60vh]', 'overflow-y-auto')}>
<motion.div className={tcls('mx-auto w-full max-w-prose')} layout="position">
{askState?.type === 'answer' ? (
<React.Suspense fallback={loading}>
<TransitionAnswerBody answer={askState.answer} placeholder={loading} />
Expand All @@ -104,7 +111,7 @@ export function SearchAskAnswer(props: { query: string }) {
<div className={tcls('p-4')}>{t(language, 'search_ask_error')}</div>
) : null}
{askState?.type === 'loading' ? loading : null}
</div>
</motion.div>
);
}

Expand Down Expand Up @@ -138,10 +145,7 @@ function AnswerBody(props: { answer: AskAnswerResult }) {

return (
<>
<div
data-testid="search-ask-answer"
className={tcls('my-4', 'sm:mt-6', 'px-4', 'sm:px-12', 'text-tint-strong')}
>
<div data-testid="search-ask-answer" className={tcls('text-tint-strong')}>
{answer.body ?? t(language, 'search_ask_no_answer')}
{answer.followupQuestions.length > 0 ? (
<AnswerFollowupQuestions followupQuestions={answer.followupQuestions} />
Expand Down Expand Up @@ -182,7 +186,6 @@ function AnswerFollowupQuestions(props: { followupQuestions: string[] }) {
)}
{...getSearchLinkProps({
query: question,
ask: true,
})}
>
<Icon
Expand Down
4 changes: 2 additions & 2 deletions packages/gitbook/src/components/Search/SearchButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function SearchButton(props: { children?: React.ReactNode; style?: ClassV

const onClick = () => {
setSearchState({
ask: false,
mode: 'both',
global: false,
query: '',
});
Expand Down Expand Up @@ -99,7 +99,7 @@ export function SearchButton(props: { children?: React.ReactNode; style?: ClassV
);
}

function Shortcut() {
export function Shortcut() {
const [operatingSystem, setOperatingSystem] = useState<string | null>(null);

useEffect(() => {
Expand Down
Loading
Loading