Skip to content

Commit 9c5c1e5

Browse files
scazanSamyPesse
andauthored
Show sources with no answer (#2363)
Co-authored-by: Samy Pessé <[email protected]>
1 parent 6929b0b commit 9c5c1e5

File tree

11 files changed

+160
-107
lines changed

11 files changed

+160
-107
lines changed

bun.lockb

360 Bytes
Binary file not shown.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
],
2121
"dependencies": {
2222
"@geist-ui/icons": "^1.0.2",
23-
"@gitbook/api": "^0.51.0",
23+
"@gitbook/api": "^0.52.0",
2424
"@radix-ui/react-checkbox": "^1.0.4",
2525
"@radix-ui/react-popover": "^1.0.7",
2626
"@sentry/nextjs": "^7.94.1",

src/components/Search/SearchAskAnswer.tsx

Lines changed: 116 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import { atom, useRecoilState } from 'recoil';
66
import { Loading } from '@/components/primitives';
77
import { useLanguage } from '@/intl/client';
88
import { t } from '@/intl/translate';
9+
import { TranslationLanguage } from '@/intl/translations';
910
import { iterateStreamResponse } from '@/lib/actions';
1011
import { tcls } from '@/lib/tailwind';
1112

12-
import { AskAnswerResult, streamAskQuestion } from './server-actions';
13+
import { AskAnswerResult, AskAnswerSource, streamAskQuestion } from './server-actions';
1314
import { useSearch, useSearchLink } from './useSearch';
1415
import { Link } from '../primitives';
1516

@@ -94,6 +95,11 @@ export function SearchAskAnswer(props: { spaceId: string; query: string }) {
9495
};
9596
}, [setState]);
9697

98+
let hasAnswer = false;
99+
if (state && 'answer' in state) {
100+
hasAnswer = !!state?.answer?.body;
101+
}
102+
97103
return (
98104
<div
99105
className={tcls(
@@ -129,7 +135,6 @@ export function SearchAskAnswer(props: { spaceId: string; query: string }) {
129135

130136
function AnswerBody(props: { answer: AskAnswerResult }) {
131137
const { answer } = props;
132-
const getSearchLinkProps = useSearchLink();
133138
const language = useLanguage();
134139

135140
const [, setSearchState] = useSearch();
@@ -143,94 +148,123 @@ function AnswerBody(props: { answer: AskAnswerResult }) {
143148
data-test="search-ask-answer"
144149
className={tcls('mt-4', 'px-4', 'text-dark/9', 'dark:text-light/8')}
145150
>
146-
{answer.body}
151+
{answer.hasAnswer ? answer.body : t(language, 'search_ask_no_answer')}
147152
</div>
148153
{answer.followupQuestions.length > 0 ? (
149-
<div className={tcls('mt-7', 'flex', 'flex-col', 'flex-wrap', 'gap-1')}>
150-
{answer.followupQuestions.map((question) => (
151-
<Link
152-
key={question}
153-
className={tcls(
154-
'text-sm',
155-
'font-medium',
156-
'inline-flex',
157-
'items-start',
158-
'gap-2',
159-
'px-4',
160-
'py-1',
161-
'text-primary-500',
162-
'focus-within:text-primary-700',
163-
'hover:bg-primary/2',
164-
'dark:text-primary-400',
165-
'dark:hover:bg-primary-500/3',
166-
)}
167-
{...getSearchLinkProps({
168-
query: question,
169-
ask: true,
170-
})}
171-
>
172-
<IconSearch
173-
className={tcls(
174-
'w-[15px]',
175-
'h-[15px]',
176-
'shrink-0',
177-
'mt-0.5',
178-
'[&>path]:[stroke-opacity:0.64]',
179-
)}
180-
/>
181-
<span>{question}</span>
182-
</Link>
183-
))}
184-
</div>
154+
<AnswerFollowupQuestions followupQuestions={answer.followupQuestions} />
185155
) : null}
186156
{answer.sources.length > 0 ? (
187-
<div
157+
<AnswerSources
158+
hasAnswer={answer.hasAnswer}
159+
sources={answer.sources}
160+
language={language}
161+
onClose={onClose}
162+
/>
163+
) : null}
164+
</>
165+
);
166+
}
167+
168+
function AnswerFollowupQuestions(props: { followupQuestions: string[] }) {
169+
const { followupQuestions } = props;
170+
const getSearchLinkProps = useSearchLink();
171+
172+
return (
173+
<div className={tcls('mt-7 mb-4', 'flex', 'flex-col', 'flex-wrap', 'gap-1')}>
174+
{followupQuestions.map((question) => (
175+
<Link
176+
key={question}
188177
className={tcls(
189-
'flex',
190-
'flex-wrap',
178+
'text-sm',
179+
'font-medium',
180+
'inline-flex',
181+
'items-start',
191182
'gap-2',
192-
'mt-7',
193-
'py-4',
194183
'px-4',
195-
'border-t',
196-
'border-dark/2',
197-
'dark:border-light/1',
184+
'py-1',
185+
'text-primary-500',
186+
'focus-within:text-primary-700',
187+
'hover:bg-primary/2',
188+
'dark:text-primary-400',
189+
'dark:hover:bg-primary-500/3',
198190
)}
191+
{...getSearchLinkProps({
192+
query: question,
193+
ask: true,
194+
})}
199195
>
200-
<span className={tcls('text-sm')}>{t(language, 'search_ask_sources')}</span>
201-
202-
{answer.sources.map((source) => (
203-
<span key={source.id} className={tcls()}>
204-
<Link
205-
onClick={onClose}
206-
className={tcls(
207-
'flex',
208-
'text-sm',
209-
'text-dark/7',
210-
'hover:underline',
211-
'focus-within:text-primary-700',
212-
'dark:text-light/8',
213-
)}
214-
href={source.href}
215-
prefetch={false}
216-
>
217-
<IconBox
218-
className={tcls(
219-
'stroke-dark/6',
220-
'w-[15px]',
221-
'h-[15px]',
222-
'shrink-0',
223-
'mt-0.5',
224-
'mr-0.5',
225-
'dark:stroke-light/6',
226-
)}
227-
/>
228-
{source.title}
229-
</Link>
230-
</span>
231-
))}
232-
</div>
233-
) : null}
234-
</>
196+
<IconSearch
197+
className={tcls(
198+
'w-[15px]',
199+
'h-[15px]',
200+
'shrink-0',
201+
'mt-0.5',
202+
'[&>path]:[stroke-opacity:0.64]',
203+
)}
204+
/>
205+
<span>{question}</span>
206+
</Link>
207+
))}
208+
</div>
209+
);
210+
}
211+
212+
function AnswerSources(props: {
213+
sources: AskAnswerSource[];
214+
language: TranslationLanguage;
215+
onClose: () => void;
216+
hasAnswer?: boolean;
217+
}) {
218+
const { sources, onClose, language, hasAnswer } = props;
219+
220+
return (
221+
<div
222+
className={tcls(
223+
'flex',
224+
'flex-wrap',
225+
'gap-2',
226+
'mt-7',
227+
'py-4',
228+
'px-4',
229+
'border-t',
230+
'border-dark/2',
231+
'dark:border-light/1',
232+
)}
233+
>
234+
<span className={tcls('text-sm')}>
235+
{t(language, hasAnswer ? 'search_ask_sources' : 'search_ask_sources_no_answer')}
236+
</span>
237+
238+
{sources.map((source) => (
239+
<span key={source.id} className={tcls()}>
240+
<Link
241+
onClick={onClose}
242+
className={tcls(
243+
'flex',
244+
'text-sm',
245+
'text-dark/7',
246+
'hover:underline',
247+
'focus-within:text-primary-700',
248+
'dark:text-light/8',
249+
)}
250+
href={source.href}
251+
prefetch={false}
252+
>
253+
<IconBox
254+
className={tcls(
255+
'stroke-dark/6',
256+
'w-[15px]',
257+
'h-[15px]',
258+
'shrink-0',
259+
'mt-0.5',
260+
'mr-0.5',
261+
'dark:stroke-light/6',
262+
)}
263+
/>
264+
{source.title}
265+
</Link>
266+
</span>
267+
))}
268+
</div>
235269
);
236270
}

src/components/Search/server-actions.tsx

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ export interface AskAnswerSource {
4646
}
4747

4848
export interface AskAnswerResult {
49-
body: React.ReactNode;
49+
body?: React.ReactNode;
5050
followupQuestions: string[];
5151
sources: AskAnswerSource[];
52+
hasAnswer: boolean;
5253
}
5354

5455
/**
@@ -112,12 +113,15 @@ export async function searchParentContent(
112113
* Server action to ask a question in a space.
113114
*/
114115
export const streamAskQuestion = streamResponse(async function* (spaceId: string, query: string) {
115-
const stream = api.api().spaces.streamAskInSpace(spaceId, { query, format: 'document' });
116+
const stream = api
117+
.api()
118+
.spaces.streamAskInSpace(spaceId, { query, format: 'document', details: true });
116119
const pagesPromise = api.getSpaceContentData({ spaceId });
117120

118121
for await (const chunk of stream) {
119122
// We run the AI search and fetch the pages in parallel
120123
const { pages } = await pagesPromise;
124+
121125
yield transformAnswer(chunk.answer, pages);
122126
}
123127
});
@@ -134,7 +138,7 @@ function transformAnswer(
134138
answer: SearchAIAnswer | undefined,
135139
pages: RevisionPage[],
136140
): AskAnswerResult | null {
137-
if (!answer || !('document' in answer.answer)) {
141+
if (!answer) {
138142
return null;
139143
}
140144

@@ -158,20 +162,22 @@ function transformAnswer(
158162
.filter(filterOutNullable);
159163

160164
return {
161-
body: (
162-
<DocumentView
163-
document={answer.answer.document}
164-
context={{
165-
mode: 'default',
166-
contentRefContext: null,
167-
resolveContentRef: async () => null,
168-
shouldHighlightCode: () => false,
169-
}}
170-
style={['space-y-5']}
171-
/>
172-
),
165+
body:
166+
answer.answer && 'document' in answer.answer ? (
167+
<DocumentView
168+
document={answer.answer.document}
169+
context={{
170+
mode: 'default',
171+
contentRefContext: null,
172+
resolveContentRef: async () => null,
173+
shouldHighlightCode: () => false,
174+
}}
175+
style={['space-y-5']}
176+
/>
177+
) : null,
173178
followupQuestions: answer.followupQuestions,
174179
sources,
180+
hasAnswer: !!answer.answer && 'document' in answer.answer,
175181
};
176182
}
177183

src/intl/translations/de.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ export const de = {
1313
search_scope_all: 'Alle Inhalte',
1414
search_ask: 'Fragen "${1}"',
1515
search_ask_sources: 'Quellen',
16-
search_ask_no_answer: 'Es konnte keine Antwort auf Ihre Frage gefunden werden, versuchen Sie es mit einer anderen Frage.',
16+
search_ask_sources_no_answer: 'Verwandte Seiten',
17+
search_ask_no_answer:
18+
'Es konnte keine Antwort auf Ihre Frage gefunden werden. Versuchen Sie, sie umzuformulieren oder genauer zu sein.',
1719
search_ask_error: 'Etwas ist schief gelaufen. Bitte versuchen Sie es später noch einmal.',
1820
on_this_page: 'Auf dieser Seite',
1921
next_page: 'Nächste',
@@ -39,14 +41,16 @@ export const de = {
3941
notfound_title: 'Seite nicht gefunden',
4042
notfound: 'Die gesuchte Seite existiert nicht.',
4143
unexpected_error_title: 'Ein Fehler ist aufgetreten',
42-
unexpected_error: 'Entschuldigung, ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.',
44+
unexpected_error:
45+
'Entschuldigung, ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.',
4346
unexpected_error_retry: 'Erneut versuchen',
4447
pdf_download: 'Als PDF exportieren',
4548
pdf_goback: 'Zurück zum Inhalt',
4649
pdf_print: 'Drucken oder als PDF speichern',
4750
pdf_page_of: '${1} von ${2}',
4851
pdf_mode_only_page: 'Nur diese Seite',
4952
pdf_mode_all: 'Alle Seiten',
50-
pdf_limit_reached: 'Das PDF konnte für ${1} Seiten nicht generiert werden, Generierung wurde bei ${2} gestoppt.',
53+
pdf_limit_reached:
54+
'Das PDF konnte für ${1} Seiten nicht generiert werden, Generierung wurde bei ${2} gestoppt.',
5155
pdf_limit_reached_continue: 'Mit ${1} weiteren Seiten erweitern.',
5256
};

src/intl/translations/en.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ export const en = {
1313
search_scope_all: 'All the content',
1414
search_ask: 'Ask "${1}"',
1515
search_ask_sources: 'Sources',
16-
search_ask_no_answer: 'No answer could be found for your question, try with another one.',
16+
search_ask_sources_no_answer: 'Related pages',
17+
search_ask_no_answer:
18+
'An answer could not be found for your question. You could try rephrasing it, or be more specific.',
1719
search_ask_error: 'Something went wrong. Please try again later.',
1820
on_this_page: 'On this page',
1921
next_page: 'Next',

src/intl/translations/es.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ export const es: TranslationLanguage = {
1515
search_scope_all: 'Todo el contenido',
1616
search_ask: 'Preguntar "${1}"',
1717
search_ask_sources: 'Fuentes',
18-
search_ask_no_answer: 'No se pudo encontrar una respuesta a tu pregunta, intenta con otra.',
18+
search_ask_sources_no_answer: 'Páginas relacionadas',
19+
search_ask_no_answer:
20+
'No se pudo encontrar una respuesta para su pregunta. Puede intentar reformularla o ser más específico.',
1921
search_ask_error: 'Algo salió mal. Por favor, inténtalo de nuevo más tarde.',
2022
on_this_page: 'En esta página',
2123
next_page: 'Siguiente',

src/intl/translations/fr.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ export const fr: TranslationLanguage = {
1515
search_scope_all: 'Tout le contenu',
1616
search_ask: 'Demander "${1}"',
1717
search_ask_sources: 'Sources',
18-
search_ask_no_answer: 'Aucune réponse trouvée à votre question, essayez-en une autre.',
18+
search_ask_sources_no_answer: 'Pages connexes',
19+
search_ask_no_answer:
20+
"Aucune réponse n'a pu être trouvée pour votre question. Essayez de la reformuler ou d'être plus précis",
1921
search_ask_error: 'Quelque chose a mal fonctionné. Veuillez réessayer plus tard.',
2022
on_this_page: 'Sur cette page',
2123
next_page: 'Suivant',

src/intl/translations/ja.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ export const ja: TranslationLanguage = {
1515
search_scope_all: '全てのコンテンツ',
1616
search_ask: '"${1}" を質問する',
1717
search_ask_sources: '情報源',
18+
search_ask_sources_no_answer: '関連ページ',
1819
search_ask_no_answer:
19-
'ご質問に答えられる答えが見つかりませんでした、他の質問を試してください。',
20+
'ご質問への回答が見つかりませんでした。質問を言い換えるか、もう少し具体的にしてください。',
2021
search_ask_error: '何らかのエラーが発生しました。後ほど再度お試しください。',
2122
on_this_page: 'このページ内',
2223
next_page: '次へ',

0 commit comments

Comments
 (0)