Merge remote-tracking branch 'origin/main' into claude/exciting-brahmagupta-7cd6fa

# Conflicts:
#	packages/gitbook/src/components/DocumentView/OpenAPI/style.css
This commit is contained in:
Zeno Kapitein
2026-06-25 16:01:20 +02:00
95 changed files with 1328 additions and 229 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Mark the AI Assistant chat as `aria-busy` while it is generating a response (including the follow-up suggestion phase), so assistive technologies are notified of the in-progress state.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix the AI assistant panel overflowing its frame when rendering single/multi-choice question forms with long option text.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Assistant: group all of a turn's tool calls behind a single "Explored with N tools" expandable instead of rendering a separate single-item expandable for each client-side tool call.
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Trust an optional cookieless integration content host (`GITBOOK_INTEGRATIONS_CONTENT_HOST`) for WebFrame postMessage, in preparation for isolating integration-rendered content onto a separate origin. Defaults to `GITBOOK_INTEGRATIONS_HOST`, so behavior is unchanged until the content host is configured.
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Match keyboard shortcuts by the logical character typed instead of the physical key position, so that ⌘-C no longer opens the Assistant on the Dvorak layout (and other non-QWERTY layouts).
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix wide content (such as code blocks and tables) overflowing the edges of hint blocks instead of scrolling within them.
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Remove the external link button from the link tooltip hover card, as its arrow read as clickable even though the preview card isn't interactive.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Support an optional `goal` query parameter on the markdown ask interface (`?ask=…&goal=…`), letting agents describe the broader end goal they are working towards so the answer can be steered towards it.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Drive page actions ordering and default action from the new `pageActions.items` list, including the reorderable assistant action
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Add a page reference chip when opening the Assistant from the page action, so the assistant is informed about the page the reader is currently on.
-6
View File
@@ -1,6 +0,0 @@
---
"@gitbook/react-openapi": patch
"gitbook": patch
---
Support OpenAPI one-operation-per-page mode
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix grouped top-nav section dropdowns rendering empty when the site is embedded in an iframe (visitor-auth embeds, editor preview) or shown in the embeddable view. The dropdown viewport is composited and animated, and a clipped composited layer fails to rasterize its text in Chromium when painted inside a sub compositing root; the rounded-corner clipping is now done on an inner wrapper so the viewport itself is no longer clipped.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Improve the 404 page: remove the table of contents sidebar and center the content, suggest related pages (ranked from the already-cached search index, so no extra request per 404), show a search or assistant input depending on the site's AI configuration, and move "Go to homepage" to the bottom as a last resort.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Add a client-side search field to table blocks, with per-column filters for select and checkbox columns
-5
View File
@@ -1,5 +0,0 @@
---
"@gitbook/react-openapi": patch
---
Handle circular oneOf with discriminator and allOf in OpenAPI schemas
+4 -4
View File
@@ -109,7 +109,7 @@
},
"packages/gitbook": {
"name": "gitbook",
"version": "0.27.0",
"version": "0.27.1",
"dependencies": {
"@cloudflare/workers-types": "^4.20251011.0",
"@gitbook/api": "catalog:",
@@ -312,7 +312,7 @@
},
"packages/react-openapi": {
"name": "@gitbook/react-openapi",
"version": "1.5.13",
"version": "1.5.14",
"dependencies": {
"@gitbook/expr": "workspace:*",
"@gitbook/openapi-parser": "workspace:*",
@@ -360,7 +360,7 @@
"react-dom": "catalog:",
},
"catalog": {
"@gitbook/api": "0.184.0",
"@gitbook/api": "0.185.0",
"@scalar/api-client-react": "^1.3.46",
"@tsconfig/node20": "^20.1.6",
"@tsconfig/strictest": "^2.0.6",
@@ -756,7 +756,7 @@
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.2.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" } }, "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q=="],
"@gitbook/api": ["@gitbook/api@0.184.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-yPoQqLLik6IihFcwVVbuEAIyDA7F2q80HprNRuj10m+O2XULlm4+bW5R71r5/GCjDKSf5QfmRXmYh4OopljPKw=="],
"@gitbook/api": ["@gitbook/api@0.185.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-RrFZSHI7W79ri5jHd/gp01ZbuxfOPOFEqnom956wNUfUf/CIQDZcCNzmvLIWzuYesI7Snq3NLi+U5XzOsJhC4g=="],
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
+1 -1
View File
@@ -43,7 +43,7 @@
"catalog": {
"@tsconfig/strictest": "^2.0.6",
"@tsconfig/node20": "^20.1.6",
"@gitbook/api": "0.184.0",
"@gitbook/api": "0.185.0",
"@scalar/api-client-react": "^1.3.46",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
+21
View File
@@ -1,5 +1,26 @@
# gitbook
## 0.27.1
### Patch Changes
- acf2719: Mark the AI Assistant chat as `aria-busy` while it is generating a response (including the follow-up suggestion phase), so assistive technologies are notified of the in-progress state.
- 72a1596: Fix the AI assistant panel overflowing its frame when rendering single/multi-choice question forms with long option text.
- 77c717c: Assistant: group all of a turn's tool calls behind a single "Explored with N tools" expandable instead of rendering a separate single-item expandable for each client-side tool call.
- c44d822: Trust an optional cookieless integration content host (`GITBOOK_INTEGRATIONS_CONTENT_HOST`) for WebFrame postMessage, in preparation for isolating integration-rendered content onto a separate origin. Defaults to `GITBOOK_INTEGRATIONS_HOST`, so behavior is unchanged until the content host is configured.
- 7655041: Match keyboard shortcuts by the logical character typed instead of the physical key position, so that ⌘-C no longer opens the Assistant on the Dvorak layout (and other non-QWERTY layouts).
- af3cda1: Fix wide content (such as code blocks and tables) overflowing the edges of hint blocks instead of scrolling within them.
- 0ca4952: Remove the external link button from the link tooltip hover card, as its arrow read as clickable even though the preview card isn't interactive.
- 3ff88ba: Add a Prompt block
- 251630c: Drive page actions ordering and default action from the new `pageActions.items` list, including the reorderable assistant action
- ddba803: Add a page reference chip when opening the Assistant from the page action, so the assistant is informed about the page the reader is currently on.
- f4469b2: Support OpenAPI one-operation-per-page mode
- 6c967c9: Add a client-side search field to table blocks, with per-column filters for select and checkbox columns
- 882dfcd: Tidy prompt and add e2e test
- Updated dependencies [f4469b2]
- Updated dependencies [88b94b5]
- @gitbook/react-openapi@1.5.14
## 0.27.0
### Minor Changes
+5
View File
@@ -1113,6 +1113,11 @@ const testCases: TestsCase[] = [
url: 'blocks/mermaid',
run: waitForCookiesDialog,
},
{
name: 'Prompt',
url: 'blocks/prompt',
run: waitForCookiesDialog,
},
],
},
{
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gitbook",
"version": "0.27.0",
"version": "0.27.1",
"private": true,
"dependencies": {
"@cloudflare/workers-types": "^4.20251011.0",
@@ -1,4 +1,4 @@
import { SiteInsightsDisplayContext } from '@gitbook/api';
import { CustomizationPageActionType, SiteInsightsDisplayContext } from '@gitbook/api';
import { type RouteLayoutParams, getDynamicSiteContext } from '@/app/utils';
import { getExposableError, throwIfDataError } from '@/lib/data';
@@ -20,7 +20,14 @@ export async function handleMcpRequest(
const { context } = await getDynamicSiteContext(params);
const { dataFetcher, linker, site } = context;
if (!context.customization.pageActions.mcp) {
// Use the configured `items` list when the API provides it, and fall back to the deprecated
// `mcp` flag otherwise (legacy mode), since this endpoint is called directly and cannot rely on
// any page-rendering fallback.
const { pageActions } = context.customization;
const isMcpEnabled = pageActions.items
? pageActions.items.includes(CustomizationPageActionType.Mcp)
: pageActions.mcp;
if (!isMcpEnabled) {
return new Response('Not Found', { status: 404 });
}
@@ -0,0 +1,24 @@
import { type RouteLayoutParams, getStaticSiteContext } from '@/app/utils';
import { serveAskMarkdown } from '@/routes/markdownAsk';
import type { NextRequest } from 'next/server';
export const dynamic = 'force-static';
/**
* Serve an AI answer as markdown for a page, steered by the end goal ("goal")
* the calling agent provided via the `?goal=` search parameter.
*
* The goal is encoded as a path segment (rather than read from the query) because
* this route is statically rendered, mirroring how the question itself is handled.
*/
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams & { question: string; goal: string }> }
) {
const { question: encodedQuestion, goal: encodedGoal } = await params;
const { context } = await getStaticSiteContext(await params);
const question = decodeURIComponent(encodedQuestion);
const goal = decodeURIComponent(encodedGoal);
return serveAskMarkdown(context, question, { goal });
}
@@ -31,9 +31,11 @@ import { ScrollContainer } from '../primitives/ScrollContainer';
import { SideSheet } from '../primitives/SideSheet';
import { AIChatControl } from './AIChatControl';
import { AIChatControlButton } from './AIChatControlButton';
import { AIChatExpandButton } from './AIChatExpandButton';
import { AIChatIcon } from './AIChatIcon';
import { AIChatInput } from './AIChatInput';
import { AIChatMessages } from './AIChatMessages';
import { AIChatResizeHandle } from './AIChatResizeHandle';
import AIChatSuggestedQuestions from './AIChatSuggestedQuestions';
export function AIChat() {
@@ -85,9 +87,10 @@ export function AIChat() {
withOverlay={true}
data-ai-chat
className={tcls(
'ai-chat mx-auto ml-8 not-hydrated:hidden w-96 transition-[width] duration-300 ease-quint lg:max-xl:w-80'
'ai-chat mx-auto ml-8 not-hydrated:hidden w-96 transition-[width] duration-300 ease-quint lg:w-(--ai-chat-width)'
)}
>
<AIChatResizeHandle />
<EmbeddableFrame className="relative w-full shrink-0 border-tint-subtle border-l to-tint-base">
<EmbeddableFrameMain data-testid="ai-chat" aria-busy={chat.loading}>
<EmbeddableFrameHeader className="not-embed:px-4">
@@ -100,6 +103,7 @@ export function AIChat() {
</EmbeddableFrameHeaderMain>
<EmbeddableFrameButtons>
<AIChatControlButton />
<AIChatExpandButton />
<Button
onClick={() => chatController.close()}
iconOnly
@@ -0,0 +1,35 @@
'use client';
import { tString, useLanguage } from '@/intl/client';
import { Icon } from '@gitbook/icons';
import { Button } from '../primitives';
import { useAIChatWidthStore, useIsAIChatMaxWidth } from './useAIChatWidthStore';
export function AIChatExpandButton() {
const language = useLanguage();
const toggleWidth = useAIChatWidthStore((state) => state.toggleWidth);
const isMaxWidth = useIsAIChatMaxWidth();
return (
<Button
onClick={toggleWidth}
iconOnly
icon={
<Icon
icon={
isMaxWidth
? 'arrow-down-left-and-arrow-up-right-to-center'
: 'arrow-up-right-and-arrow-down-left-from-center'
}
className="scale-90"
/>
}
label={tString(
language,
isMaxWidth ? 'ai_chat_collapse_panel' : 'ai_chat_expand_panel'
)}
variant="blank"
className="max-lg:hidden"
/>
);
}
@@ -0,0 +1,83 @@
'use client';
import { tcls } from '@/lib/tailwind';
import React from 'react';
import { useAIChatWidthStore } from './useAIChatWidthStore';
function setResizing(active: boolean) {
document.documentElement.dataset.aiChatResizing = String(active);
}
export function AIChatResizeHandle() {
const setWidth = useAIChatWidthStore((state) => state.setWidth);
const frameRef = React.useRef<number | null>(null);
const widthRef = React.useRef(0);
React.useEffect(() => {
const onResize = () => useAIChatWidthStore.getState().syncWidth();
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
if (frameRef.current !== null) {
cancelAnimationFrame(frameRef.current);
}
setResizing(false);
};
}, []);
const stopResizing = (event: React.PointerEvent<HTMLDivElement>) => {
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
if (frameRef.current !== null) {
cancelAnimationFrame(frameRef.current);
frameRef.current = null;
}
setResizing(false);
};
const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
event.preventDefault();
event.currentTarget.setPointerCapture(event.pointerId);
widthRef.current = useAIChatWidthStore.getState().width;
setResizing(true);
};
const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
if (!event.currentTarget.hasPointerCapture(event.pointerId)) {
return;
}
// Panel is right-anchored, so its width is the distance from the cursor to the right edge.
widthRef.current = window.innerWidth - event.clientX;
if (frameRef.current === null) {
frameRef.current = requestAnimationFrame(() => {
frameRef.current = null;
widthRef.current = setWidth(widthRef.current);
});
}
};
const handlePointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
if (!event.currentTarget.hasPointerCapture(event.pointerId)) {
return;
}
setWidth(widthRef.current);
stopResizing(event);
};
return (
<div
aria-hidden="true"
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={stopResizing}
className={tcls(
'group -translate-x-1/2 absolute inset-y-0 left-0 z-10 hidden w-3 cursor-col-resize touch-none lg:flex',
'items-stretch justify-center'
)}
>
<span className="h-full w-px rounded-full bg-transparent transition-all duration-150 ease-out group-hover:w-0.5 group-hover:bg-primary-solid/40 group-active:w-0.5 group-active:bg-primary-solid" />
</div>
);
}
@@ -0,0 +1,78 @@
'use client';
import {
getLocalStorageItem,
removeLocalStorageItem,
setLocalStorageItem,
} from '@/lib/browser/local-storage';
import { create } from 'zustand';
import { type StorageValue, persist } from 'zustand/middleware';
const AI_CHAT_MIN_WIDTH = 384;
const AI_CHAT_MAX_WIDTH = 640;
const MIN_CONTENT_WIDTH = 720;
type AIChatWidthStore = {
width: number;
toggleWidth: () => void;
setWidth: (width: number) => number;
syncWidth: () => void;
};
type PersistedState = Pick<AIChatWidthStore, 'width'>;
export const useAIChatWidthStore = create<AIChatWidthStore>()(
persist(
(set, get) => ({
width: AI_CHAT_MIN_WIDTH,
toggleWidth: () =>
get().setWidth(
get().width >= AI_CHAT_MAX_WIDTH ? AI_CHAT_MIN_WIDTH : AI_CHAT_MAX_WIDTH
),
setWidth: (width) => {
const clamped = clampWidth(width);
if (get().width !== clamped) {
set({ width: clamped });
}
setWidthOnViewport(clamped);
return clamped;
},
syncWidth: () => setWidthOnViewport(get().width),
}),
{
name: '@gitbook/ai-chat-width',
storage: {
getItem: (name) =>
getLocalStorageItem<StorageValue<PersistedState> | null>(name, null),
setItem: (name, value) => setLocalStorageItem(name, value),
removeItem: (name) => removeLocalStorageItem(name),
},
partialize: (state) => ({ width: state.width }),
onRehydrateStorage: () => (state) => state?.syncWidth(),
}
)
);
/**
* Whether the panel is at its maximum width.
*/
export const useIsAIChatMaxWidth = () =>
useAIChatWidthStore((state) => state.width >= AI_CHAT_MAX_WIDTH);
// Hoisted so the synchronous persist rehydrate (during create() above) can call them before this point.
function setWidthOnViewport(width: number) {
if (typeof document !== 'undefined') {
document.documentElement.style.setProperty('--ai-chat-width', `${capToViewport(width)}px`);
}
}
function clampWidth(width: number) {
return Math.min(AI_CHAT_MAX_WIDTH, Math.max(AI_CHAT_MIN_WIDTH, Math.round(width)));
}
// Cap a width so the remaining content keeps a usable minimum at the current viewport.
function capToViewport(width: number) {
return typeof window === 'undefined'
? width
: Math.min(width, Math.max(AI_CHAT_MIN_WIDTH, window.innerWidth - MIN_CONTENT_WIDTH));
}
@@ -42,7 +42,7 @@ export function AnnouncementBanner(props: {
className="theme-bold:bg-header-background pt-4 pb-2"
data-nosnippet=""
>
<div className="transition-all duration-300 motion-reduce:transition-none lg:chat-open:pr-80 xl:chat-open:pr-96">
<div className="transition-all duration-300 motion-reduce:transition-none lg:chat-open:pr-(--ai-chat-width)">
<div className={tcls('relative', CONTAINER_STYLE)}>
<Tag
href={contentRef?.href ?? ''}
@@ -80,8 +80,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
'max-w-md',
'text-balance',
'sm:left-auto',
'lg:chat-open:mr-80',
'xl:chat-open:mr-100',
'lg:chat-open:mr-(--ai-chat-width)',
'transition-all',
'motion-reduce:transition-none',
'duration-300',
@@ -28,6 +28,7 @@ import { ListItem } from './ListItem';
import { BlockMath } from './Math';
import { OpenAPIOperation, OpenAPISchemas, OpenAPIWebhook } from './OpenAPI';
import { Paragraph } from './Paragraph';
import { Prompt } from './Prompt';
import { Quote } from './Quote';
import { ReusableContent } from './ReusableContent';
import { Stepper } from './Stepper';
@@ -111,6 +112,8 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
return <Updates {...props} block={block} />;
case 'update':
return <Update {...props} block={block} />;
case 'prompt':
return <Prompt {...props} block={block} />;
case 'if':
// If block should be processed by the API.
return null;
@@ -151,6 +154,7 @@ export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue }
case 'hint':
case 'tabs':
case 'stepper-step':
case 'prompt':
case 'if':
return <SkeletonParagraph id={id} className={style} />;
case 'expandable':
@@ -101,7 +101,7 @@
/* Method Tags */
.openapi-method,
.openapi-statuscode {
@apply m-0 h-5 min-w-9 justify-center rounded-md text-xs straight-corners:rounded-none circular-corners:rounded-lg uppercase font-mono items-center shrink-0 font-semibold px-1.5 py-0.5 mr-2 text-tint-12/8 leading-tight align-middle inline-flex whitespace-nowrap;
@apply m-0 h-5 min-w-9 justify-center rounded-md text-xs straight-corners:rounded-none circular-corners:rounded-lg uppercase font-mono items-center shrink-0 font-semibold px-1.5 py-0.5 text-tint-12/8 leading-tight align-middle inline-flex whitespace-nowrap;
}
.openapi-method-small {}
@@ -160,15 +160,20 @@
}
.openapi-column-preview {
@apply flex flex-col flex-1 sticky self-start max-h-[calc(100vh-var(--toc-top-offset))] scroll-mt-4 top-[calc(var(--toc-top-offset)+1.5rem)];
/* Only on the 2-column layout is the preview a sticky pane capped to the viewport so both panels fit
(max-height accounts for the top offset + a 1.5rem bottom margin). On mobile (single column) it flows
normally, each block bounded by max-h-96, so the response isn't crammed into the viewport. */
@apply flex flex-col flex-1 scroll-mt-4 lg:sticky lg:self-start lg:max-h-[calc(100vh-var(--toc-top-offset)-3rem)] lg:top-[calc(var(--toc-top-offset)+1.5rem)];
}
.openapi-column-preview-body {
@apply flex flex-col shrink overflow-hidden gap-4 print-mode:static;
@apply flex flex-col shrink min-h-0 overflow-clip [overflow-clip-margin:0.75rem] gap-4 print-mode:static;
}
.openapi-column-preview pre {
@apply max-h-96 print-mode:max-h-none;
/* On the 2-column sticky layout, let the code block grow to fill the available preview height
(bounded by the sticky max-height) instead of a fixed cap, so it doesn't scroll when there's room. */
@apply max-h-96 lg:max-h-none print-mode:max-h-none;
}
/* Schema Structure */
@@ -407,7 +412,7 @@
}
.openapi-response-tab-content {
@apply flex items-baseline truncate grow shrink max-w-max basis-full mr-auto;
@apply flex items-baseline gap-2 truncate grow shrink max-w-max basis-full mr-auto;
@apply text-left text-pretty relative leading-tight text-tint select-text;
}
@@ -543,6 +548,11 @@
@apply border shrink min-h-32 overflow-hidden rounded-corners:rounded-xl circular-corners:rounded-2xl straight-corners:rounded-xs bg-tint-subtle theme-muted:bg-tint-base theme-bold-tint:bg-tint-base border-tint-subtle depth-subtle:shadow-sm;
}
/* The code sample stays at full height; only the response example shrinks/scrolls to fit the viewport */
.openapi-codesample {
@apply shrink-0 lg:max-h-96;
}
.openapi-response-examples-panel {
@apply flex flex-col shrink overflow-hidden;
}
@@ -569,7 +579,7 @@
.openapi-panel-footer,
.openapi-codesample-footer {
@apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint empty:hidden;
@apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint empty:hidden shrink-0;
}
.openapi-panel-footer .openapi-markdown {
@@ -586,7 +596,7 @@
}
.openapi-response-examples-statuscode-title {
@apply flex items-center;
@apply flex items-center gap-2;
}
.openapi-response-examples-header .openapi-select > button,
@@ -678,6 +688,11 @@ body:has(.openapi-select-popover) {
@apply flex flex-col min-w-0 gap-0.5;
}
.openapi-select > button > span.react-aria-SelectValue:has(.openapi-statuscode),
.openapi-select-item:has(.openapi-statuscode) {
@apply gap-2;
}
.openapi-select-item [slot="description"] {
@apply text-xs text-tint-subtle;
}
@@ -704,15 +719,18 @@ body:has(.openapi-select-popover) {
/* Section Components */
.openapi-section {
@apply flex flex-col overflow-hidden;
/* min-h-0 so nested sections (e.g. the media-types panel inside a response example) can shrink
inside the height-capped preview; otherwise the code/JSON body keeps full height and pushes
the footer out of the panel. Headers/footers stay put via their own shrink-0. */
@apply flex flex-col min-h-0 overflow-clip [overflow-clip-margin:0.75rem];
}
.openapi-section-body {
@apply flex flex-col shrink overflow-hidden;
@apply flex flex-col shrink min-h-0 overflow-clip [overflow-clip-margin:0.75rem];
}
.openapi-section-header {
@apply flex flex-row items-center;
@apply flex flex-row items-center shrink-0;
}
.openapi-section-header-content {
@@ -732,7 +750,7 @@ body:has(.openapi-select-popover) {
}
.openapi-section-footer {
@apply flex flex-row items-center p-2.5 gap-2.5 text-sm text-tint-strong border-t border-tint-subtle;
@apply flex flex-row items-center p-2.5 gap-2.5 text-sm text-tint-strong border-t border-tint-subtle shrink-0;
}
.openapi-section-footer-content {
@@ -0,0 +1,65 @@
import { tcls } from '@/lib/tailwind';
import {
CustomizationPageActionType,
type DocumentBlockPrompt,
type SiteCustomizationSettings,
} from '@gitbook/api';
import { validateIconName } from '@gitbook/icons/icons';
import type { BlockProps } from '../Block';
import { getPlainCodeBlock } from '../CodeBlock/highlight';
import { PromptClient } from './PromptClient';
export function Prompt(props: BlockProps<DocumentBlockPrompt>) {
const { block } = props;
const contentIcon =
block.data.icon && validateIconName(block.data.icon) ? block.data.icon : null;
return (
<div
className={tcls(
'relative flex w-full flex-col overflow-hidden',
'border border-tint-subtle bg-tint-subtle theme-bold-tint:bg-tint-base theme-muted:bg-tint-base text-tint-strong contrast-more:border-tint contrast-more:bg-tint-base',
'circular-corners:rounded-2xl rounded-corners:rounded-xl straight-corners:rounded-xs',
'depth-subtle:shadow-xs'
)}
>
<PromptClient
contentIcon={contentIcon}
description={block.data.description}
prompt={getPromptText(block)}
openInAIProviders={getOpenInAIProviders(props)}
/>
</div>
);
}
function getOpenInAIProviders(props: BlockProps<DocumentBlockPrompt>): boolean {
const { block, context } = props;
const { openInAIProviders } = block.data;
if (openInAIProviders !== undefined) {
return openInAIProviders;
}
const contentContext = context.contentContext;
if (contentContext && 'customization' in contentContext) {
const { pageActions } = contentContext.customization;
return isExternalAIPageActionEnabled(pageActions);
}
return false;
}
function isExternalAIPageActionEnabled(
pageActions: SiteCustomizationSettings['pageActions']
): boolean {
// Use the configured `items` list when the API provides it, and fall back to the deprecated
// `externalAI` flag otherwise (legacy mode).
return pageActions.items
? pageActions.items.includes(CustomizationPageActionType.ExternalAi)
: pageActions.externalAI;
}
function getPromptText(block: DocumentBlockPrompt): string {
return (block.nodes ?? []).map((node) => getPlainCodeBlock(node)).join('\n');
}
@@ -0,0 +1,212 @@
'use client';
import {
Button,
ButtonGroup,
DropdownMenu,
DropdownMenuItem,
ToggleChevron,
} from '@/components/primitives';
import { getURLForLLM } from '@/components/utils';
import { tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { Icon, type IconName } from '@gitbook/icons';
import React from 'react';
const OPEN_IN_AI_PROVIDERS = ['claude', 'chatgpt', 'cursor'] as const;
type AIProviders = (typeof OPEN_IN_AI_PROVIDERS)[number];
export function PromptClient(props: {
contentIcon: IconName | null;
description: string;
prompt: string;
openInAIProviders: boolean;
}) {
const { contentIcon, description, prompt, openInAIProviders } = props;
const language = useLanguage();
const promptId = React.useId();
const [open, setOpen] = React.useState(false);
const [headerHasFocus, setHeaderHasFocus] = React.useState(false);
return (
<>
<div className="group/prompt-header relative flex min-h-9 flex-row items-center justify-between gap-4 px-3 py-2">
<button
type="button"
aria-controls={promptId}
aria-expanded={open}
aria-label={tString(language, 'view')}
className={tcls(
'absolute inset-0 z-10 cursor-pointer outline-hidden',
'focus-visible:ring-2 focus-visible:ring-primary-hover'
)}
disabled={!prompt}
onBlur={() => setHeaderHasFocus(false)}
onClick={() => setOpen((prev) => !prev)}
onFocus={() => setHeaderHasFocus(true)}
/>
<div className="pointer-events-none relative z-0 flex min-w-0 flex-row items-center gap-2 text-tint-strong">
<PromptDisclosureIcon
contentIcon={contentIcon}
headerHasFocus={headerHasFocus}
open={open}
/>
<span className="min-w-0 truncate">{description}</span>
</div>
<PromptActions prompt={prompt} openInAIProviders={openInAIProviders} />
</div>
{open ? (
<div id={promptId} className="border-tint-subtle border-t bg-tint-base">
<pre className="overflow-auto p-4 text-sm text-tint-strong">
<code className="language-markdown whitespace-pre-wrap font-mono">
{prompt}
</code>
</pre>
</div>
) : null}
</>
);
}
function PromptDisclosureIcon(props: {
contentIcon: IconName | null;
headerHasFocus: boolean;
open: boolean;
}) {
const { contentIcon, headerHasFocus, open } = props;
return (
<span className="relative flex size-4 shrink-0 items-center justify-center">
{contentIcon ? (
<>
<span
className={tcls(
'flex items-center transition-opacity duration-150 group-hover/prompt-header:opacity-0',
headerHasFocus && 'opacity-0'
)}
>
<Icon icon={contentIcon} className="size-4 shrink-0" />
</span>
<span
className={tcls(
'absolute inset-0 flex items-center justify-center text-tint-subtle opacity-0 transition-opacity duration-150 group-hover/prompt-header:opacity-100',
headerHasFocus && 'opacity-100'
)}
>
<ToggleChevron open={open} orientation="right-to-down" className="size-3" />
</span>
</>
) : (
<ToggleChevron
open={open}
orientation="right-to-down"
className="size-3 text-tint-subtle"
/>
)}
</span>
);
}
function PromptActions(props: { prompt: string; openInAIProviders: boolean }) {
const { prompt, openInAIProviders } = props;
return (
<ButtonGroup className="relative z-20 shrink-0 overflow-visible">
<CopyPromptButton prompt={prompt} />
{openInAIProviders ? <OpenPromptDropdown prompt={prompt} /> : null}
</ButtonGroup>
);
}
// time in milliseconds to show the "Copied" message after copying a prompt
const COPIED_MESSAGE_DURATION = 1000;
function CopyPromptButton(props: { prompt: string }) {
const { prompt } = props;
const language = useLanguage();
const [copied, setCopied] = React.useState(false);
React.useEffect(() => {
if (!copied) {
return;
}
const timeout = setTimeout(() => {
setCopied(false);
}, COPIED_MESSAGE_DURATION);
return () => {
clearTimeout(timeout);
};
}, [copied]);
return (
<Button
variant="secondary"
size="xsmall"
icon={copied ? 'check' : 'copy'}
label={copied ? tString(language, 'code_copied') : tString(language, 'prompt_copy')}
className="bg-tint-base"
disabled={!prompt}
onClick={() => {
navigator.clipboard.writeText(prompt);
setCopied(true);
}}
/>
);
}
function OpenPromptDropdown(props: { prompt: string }) {
const { prompt } = props;
const language = useLanguage();
return (
<DropdownMenu
align="end"
className="!min-w-48 max-w-max"
button={
<Button
icon={<ToggleChevron className="size-text-sm" />}
label={tString(language, 'open')}
iconOnly
size="xsmall"
variant="secondary"
className="bg-tint-base"
disabled={!prompt}
/>
}
>
{OPEN_IN_AI_PROVIDERS.map((provider) => {
const definition = getPromptOpenActionDefinition(provider);
return (
<DropdownMenuItem
key={provider}
href={getURLForLLM(provider, prompt)}
target="_blank"
leadingIcon={definition.icon}
>
{tString(language, 'open_in', definition.label)}
</DropdownMenuItem>
);
})}
</DropdownMenu>
);
}
function getPromptOpenActionDefinition(action: AIProviders): { icon: IconName; label: string } {
switch (action) {
case 'cursor':
return {
icon: 'cursor',
label: 'Cursor',
};
case 'claude':
return {
icon: 'claude',
label: 'Claude',
};
case 'chatgpt':
return {
icon: 'chatgpt',
label: 'ChatGPT',
};
}
}
@@ -0,0 +1,2 @@
export * from './Prompt';
export type * from './types';
@@ -0,0 +1,14 @@
import type { DocumentBlockCode } from '@gitbook/api';
export type PromptBlock = {
object: 'block';
type: 'prompt';
key?: string;
data: {
icon?: string;
description?: string;
openInAIProviders?: boolean;
};
nodes?: DocumentBlockCode[];
isVoid?: false;
};
@@ -38,7 +38,10 @@ export function Table(props: BlockProps<DocumentBlockTable>) {
a[1].orderIndex.localeCompare(b[1].orderIndex)
);
const showSearch = context.mode !== 'print' && records.length >= MIN_RECORDS_FOR_SEARCH;
const showSearch =
context.mode !== 'print' &&
records.length >= MIN_RECORDS_FOR_SEARCH &&
block.data.view.type === 'grid'; // Tables only for now
const searchRecords = showSearch
? records.map(([id, record]) => ({ id, ...getTableRecordSearchData(block, record) }))
: [];
@@ -35,7 +35,7 @@ export function Footer(props: { context: GitBookSiteContext }) {
mobileOnly ? 'xl:hidden' : null
)}
>
<div className="transition-[padding] duration-300 motion-reduce:transition-none lg:chat-open:pr-80 xl:chat-open:pr-96">
<div className="transition-[padding] duration-300 motion-reduce:transition-none lg:chat-open:pr-(--ai-chat-width)">
<div
className={tcls(
CONTAINER_STYLE,
@@ -75,7 +75,7 @@ export async function Header(props: {
'site-header:theme-bold:shadow-tint-12/2'
)}
>
<div className="transition-all duration-300 motion-reduce:transition-none lg:chat-open:pr-80 xl:chat-open:pr-96">
<div className="transition-all duration-300 motion-reduce:transition-none lg:chat-open:pr-(--ai-chat-width)">
<div
data-gb-header-content
className={tcls(
@@ -206,7 +206,7 @@ export async function Header(props: {
</div>
{visibleSections && withSections ? (
<div className="transition-[padding] duration-300 motion-reduce:transition-none lg:chat-open:pr-80 xl:chat-open:pr-96">
<div className="transition-[padding] duration-300 motion-reduce:transition-none lg:chat-open:pr-(--ai-chat-width)">
<SiteSectionTabs sections={encodeClientSiteSections(context, visibleSections)}>
{variants.translations.length > 1 ? (
<TranslationsDropdown
@@ -4,8 +4,8 @@ import { useAIChatController, useAIChatState } from '@/components/AI';
import type { Assistant } from '@/components/AI';
import { Button } from '@/components/primitives/Button';
import { DropdownMenuItem, useDropdownMenuClose } from '@/components/primitives/DropdownMenu';
import { getURLForLLM } from '@/components/utils';
import { tString, useLanguage } from '@/intl/client';
import type { TranslationLanguage } from '@/intl/translations';
import type { GitSyncState } from '@gitbook/api';
import { Icon, type IconName, IconStyle } from '@gitbook/icons';
import assertNever from 'assert-never';
@@ -206,7 +206,7 @@ export function ActionOpenInLLM(props: {
const language = useLanguage();
const providerLabel = provider === 'chatgpt' ? 'ChatGPT' : 'Claude';
const prompt = tString(language, 'open_in_llms_pre_prompt', url);
return (
<PageActionWrapper
type={type}
@@ -214,7 +214,7 @@ export function ActionOpenInLLM(props: {
label={tString(language, 'open_in', providerLabel)}
shortLabel={providerLabel}
description={tString(language, 'ai_chat_ask_about_page', providerLabel)}
href={getLLMURL(provider, url, language)}
href={getURLForLLM(provider, prompt)}
/>
);
}
@@ -512,19 +512,3 @@ function PageActionWrapper(props: {
</DropdownMenuItem>
);
}
/**
* Returns the URL to open the page in a LLM with a pre-filled prompt.
*/
function getLLMURL(provider: 'chatgpt' | 'claude', url: string, language: TranslationLanguage) {
const prompt = encodeURIComponent(tString(language, 'open_in_llms_pre_prompt', url));
switch (provider) {
case 'chatgpt':
return `https://chat.openai.com/?q=${prompt}`;
case 'claude':
return `https://claude.ai/new?q=${prompt}`;
default:
assertNever(provider);
}
}
@@ -108,6 +108,9 @@ export function ScrollSectionsList({ sections }: { sections: DocumentSection[] }
'border-transparent',
'sidebar-list-line:-left-px',
// The method badge no longer has a right margin, so lay the row out with a gap
section.tag && ['flex', 'items-baseline', 'gap-2'],
section.depth > 1 && [
'subitem',
'sidebar-list-line:pl-6',
@@ -146,6 +149,7 @@ export function ScrollSectionsList({ sections }: { sections: DocumentSection[] }
method={section.tag}
size="small"
className="-mt-0.5"
short
/>
) : null}
@@ -2,7 +2,12 @@ import type { GitBookSiteContext } from '@/lib/context';
import type { AncestorRevisionPage } from '@/lib/pages';
import { tcls } from '@/lib/tailwind';
import { getPageRSSURL } from '@/routes/rss';
import { CustomizationAIMode, type RevisionPageDocument, SiteVisibility } from '@gitbook/api';
import {
CustomizationAIMode,
CustomizationPageActionType,
type RevisionPageDocument,
SiteVisibility,
} from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import urlJoin from 'url-join';
import { getPDFURLSearchParams } from '../PDF';
@@ -29,18 +34,18 @@ export async function PageHeader(props: {
const pageActionsEnabled = page.layout.actions !== false;
// Show page actions if *any* of the actions are enabled
const hasPageActions =
pageActionsEnabled &&
[
context.customization.ai.mode === CustomizationAIMode.Assistant,
context.customization.pageActions.externalAI,
context.customization.pageActions.markdown,
context.customization.pageActions.mcp,
context.customization.pdf.enabled,
context.customization.git.showEditLink,
withRSSFeed,
].some(Boolean);
// Show page actions if *any* of the configured actions are enabled, or if the RSS feed is
// available. RSS is contextual (only on update/blog index pages) and is not part of the
// configured `items` list, so it is checked separately.
const hasConfiguredPageActions = [
CustomizationPageActionType.Assistant,
CustomizationPageActionType.ExternalAi,
CustomizationPageActionType.Markdown,
CustomizationPageActionType.Mcp,
CustomizationPageActionType.Pdf,
CustomizationPageActionType.Git,
].some((type) => isPageActionEnabled(context.customization, type));
const hasPageActions = pageActionsEnabled && (hasConfiguredPageActions || withRSSFeed);
if (!page.layout.title && !page.layout.description && !hasPageActions) {
return null;
@@ -164,13 +169,15 @@ function getPageActionsURLs({
markdown: `${context.linker.toAbsoluteURL(context.linker.toPathInSpace(page.path))}.md`,
rss: withRSSFeed ? getPageRSSURL(context, page) : undefined,
editOnGit:
context.customization.git.showEditLink && context.space.gitSync?.url && page.git
isPageActionEnabled(context.customization, CustomizationPageActionType.Git) &&
context.space.gitSync?.url &&
page.git
? {
provider: context.space?.gitSync?.installationProvider,
url: urlJoin(context.space.gitSync.url, page.git.path),
}
: undefined,
pdf: context.customization.pdf.enabled
pdf: isPageActionEnabled(context.customization, CustomizationPageActionType.Pdf)
? context.linker.toPathInSpace(
`~gitbook/pdf?${getPDFURLSearchParams({
page: page.id,
@@ -183,6 +190,37 @@ function getPageActionsURLs({
};
}
/**
* Whether a given built-in page action is enabled. Uses the configured `items` list when the API
* provides it, and falls back to the deprecated boolean flags otherwise (legacy mode), matching the
* fallback used by the page actions dropdown.
*/
function isPageActionEnabled(
customization: GitBookSiteContext['customization'],
type: CustomizationPageActionType
): boolean {
const { pageActions } = customization;
if (pageActions.items) {
return pageActions.items.includes(type);
}
switch (type) {
case CustomizationPageActionType.ExternalAi:
return pageActions.externalAI;
case CustomizationPageActionType.Markdown:
return pageActions.markdown;
case CustomizationPageActionType.Mcp:
return pageActions.mcp;
case CustomizationPageActionType.Pdf:
return customization.pdf.enabled;
case CustomizationPageActionType.Git:
return customization.git.showEditLink;
case CustomizationPageActionType.Assistant:
return customization.ai.mode === CustomizationAIMode.Assistant;
default:
return false;
}
}
/**
* Return the MCP URL to be used in the page actions dropdown.
*/
@@ -211,6 +211,7 @@
@apply leading-relaxed;
interpolate-size: allow-keywords; /* Opt-in for modern browsers to interpolate "auto" values in transitions/animations. */
overflow-x: hidden; /* We never want horizontal scroll of the whole page, it looks buggy and we should never have overflow anyway */
--ai-chat-width: 24rem; /* Default AI chat panel width (= AI_CHAT_DEFAULT_WIDTH 384px); overridden client-side from local storage. */
}
/* Modern browsers with `scrollbar-*` support */
@@ -305,6 +306,15 @@ html.dark {
color-scheme: dark light;
}
/** While the AI chat panel is being resized, suppress transitions */
html[data-ai-chat-resizing="true"] {
cursor: col-resize;
user-select: none;
}
html[data-ai-chat-resizing="true"] * {
transition-duration: 0s !important;
}
html.announcement-hidden [data-gb-announcement-banner] {
@apply hidden;
}
@@ -1,89 +1,290 @@
'use client';
import { TrackPageViewEvent } from '@/components/Insights';
import { t, useLanguage } from '@/intl/client';
import { t, tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { SiteInsightsDisplayContext } from '@gitbook/api';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
import { Icon, type IconName } from '@gitbook/icons';
import leven from 'leven';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useAI } from '../AI';
import { PreservePageLayout } from '../PageBody/PreservePageLayout';
import { useSetSearchState } from '../Search';
import { SiteAuthLoginButton } from '../SiteAuth/SiteAuthLoginLink';
import { useSiteAdaptiveAuthLoginHref, useSpaceBasePath } from '../SpaceLayout/SpaceLayoutContext';
import { CurrentPageProvider } from '../hooks';
import { Button, SuspenseLoadedHint } from '../primitives';
import {
useSiteAdaptiveAuthLoginHref,
useSiteIndexURL,
useSpaceBasePath,
} from '../SpaceLayout/SpaceLayoutContext';
import { CurrentPageProvider, useCurrentContent } from '../hooks';
import { Button, Emoji, SkeletonList, StyledLink, SuspenseLoadedHint } from '../primitives';
import { Input } from '../primitives/Input';
const RELATED_PAGES_COUNT = 5;
type RelatedPage = { id: string; title: string; href: string; emoji?: string; icon?: string };
/**
* Component that displays a "page not found" message.
* Component that displays a "page not found" message, with ways to recover: related pages,
* the assistant (when configured), search, login (for adaptive content), and the home page.
*/
export function SitePageNotFound() {
const basePath = useSpaceBasePath();
const adaptiveAuthLoginHref = useSiteAdaptiveAuthLoginHref();
const siteIndexURL = useSiteIndexURL();
const { siteSpaceId } = useCurrentContent();
const language = useLanguage();
const router = useRouter();
const searchParams = useSearchParams();
const pathname = usePathname();
const setSearchState = useSetSearchState();
const { assistants } = useAI();
// Show the assistant input when a non-search assistant is available (i.e. not just ask-AI).
const assistant = assistants.find((candidate) => candidate.mode !== 'search') ?? null;
const inputLabel = tString(language, assistant ? 'search_or_ask' : 'search');
// If ?fallback=true, redirect to the root page.
const fallback = searchParams?.get('fallback');
const ask = searchParams?.get('ask');
// Related-page suggestions: `null` while loading, `[]` when there are none.
const [suggestions, setSuggestions] = useState<RelatedPage[] | null>(null);
useEffect(() => {
// ?fallback and ?ask redirect away from the 404. The ?ask redirect also avoids an infinite
// RSC refetch loop here: leaving it set would rerender the page and restart the assistant.
if (fallback) {
router.replace(basePath);
} else if (ask) {
// This is a workaround to prevent an infinite loop when the ask parameter is present.
// If it is present while we are on a 404 page, Next.js will fetch the RSC of the page again,
// trigerring the page to rerender and the assistant to start again, causing the infinite loop.
// To prevent this, when we detect the ask parameter, we redirect to the base path where the error does not occur.
router.replace(`${basePath}?${searchParams?.toString()}`);
return;
}
}, [basePath, fallback, ask, router, searchParams]);
if (ask) {
router.replace(`${basePath}?${searchParams?.toString()}`);
return;
}
// Otherwise, rank the site's pages against the path that 404'd. We reuse the search index
// (already preloaded and CDN-cached), so this adds no origin request — see getRelatedPages.
let active = true;
getRelatedPages(siteIndexURL, pathname ?? '', siteSpaceId).then(
(pages) => {
if (active) {
setSuggestions(pages);
}
},
() => {
if (active) {
setSuggestions([]);
}
}
);
return () => {
active = false;
};
}, [siteIndexURL, siteSpaceId, fallback, ask, pathname, basePath, router, searchParams]);
const openSearch = (value?: string) => {
setSearchState((prev) => ({
ask: prev?.ask ?? null,
scope: prev?.scope ?? 'default',
query: value ?? prev?.query ?? '',
open: true,
}));
};
return (
<CurrentPageProvider page={null}>
<div
className={tcls(
'layout-default',
'page-has-toc',
'flex-1',
'flex',
'flex-row',
'items-center',
'justify-center',
'py-9',
'min-h-[calc(100vh-64px)] lg:min-h-fit'
)}
>
<PreservePageLayout wideLayout={false} pageHasToc={true} />
<div className={tcls('max-w-80')}>
<h2 className={tcls('text-2xl', 'font-semibold', 'mb-2')}>
{t(
language,
adaptiveAuthLoginHref ? 'notfound_adaptive_title' : 'notfound_title'
)}
</h2>
<p className={tcls('text-base', 'mb-4')}>
{t(language, adaptiveAuthLoginHref ? 'notfound_adaptive' : 'notfound')}
</p>
<div className="flex gap-2">
{adaptiveAuthLoginHref ? (
<SiteAuthLoginButton
href={adaptiveAuthLoginHref}
variant="primary"
size="medium"
label={t(language, 'notfound_adaptive_login')}
>
{t(language, 'notfound_adaptive_login')}
</SiteAuthLoginButton>
) : null}
<Button href={basePath} variant="secondary">
{t(language, 'notfound_goto_home')}
</Button>
</div>
</div>
<SuspenseLoadedHint />
<div className="lg:has-sidebar:-ml-84 w-full">
<main
className={tcls(
// `page-no-toc` hides the TOC (like PageBody), and `layout-wide` takes the
// now-empty sidebar out of flow so the content stays centered on all layouts.
'page-no-toc',
'layout-wide',
'mx-auto',
'w-full',
'max-w-3xl',
'flex',
'flex-col',
'items-center',
'py-[10vh]',
'gap-16'
)}
>
<PreservePageLayout wideLayout={true} pageHasToc={false} />
{/* Track the page not found as a page view */}
<TrackPageViewEvent displayContext={SiteInsightsDisplayContext.Site} />
<div className="flex w-full flex-col items-center gap-8">
<div className="flex w-full flex-col items-center gap-2 rounded-3xl">
<h1 className={tcls('text-3xl', 'font-semibold', 'text-tint-strong')}>
{t(
language,
adaptiveAuthLoginHref
? 'notfound_adaptive_title'
: 'notfound_title'
)}
</h1>
<p className={tcls('text-base', 'text-tint')}>
{t(
language,
adaptiveAuthLoginHref ? 'notfound_adaptive' : 'notfound'
)}
</p>
</div>
{adaptiveAuthLoginHref ? (
<div className="flex flex-col items-center gap-4">
<SiteAuthLoginButton
href={adaptiveAuthLoginHref}
variant="primary"
size="large"
label={t(language, 'notfound_adaptive_login')}
className="w-full max-w-64 justify-center"
/>
<p className="text-center text-sm text-tint-subtle">
{t(language, 'notfound_adaptive_registration_hint')}
</p>
</div>
) : null}
</div>
<div className="flex w-full flex-col">
<Input
label={inputLabel}
placeholder={`${inputLabel}`}
sizing="large"
leading={assistant ? assistant.icon : 'search'}
onSubmit={(value) =>
assistant ? assistant.open(value) : openSearch(value)
}
submitButton={{ label: inputLabel }}
maxLength={2048}
className="grow"
/>
<NotFoundSuggestions suggestions={suggestions} />
</div>
{/* "Go to homepage" is the last resort, kept at the end of the flow. */}
<Button
href={basePath}
variant="blank"
icon="home"
size="small"
className="-ml-3 text-base"
label={tString(language, 'notfound_goto_home')}
/>
<SuspenseLoadedHint />
{/* Track the page not found as a page view */}
<TrackPageViewEvent displayContext={SiteInsightsDisplayContext.Site} />
</main>
</div>
</CurrentPageProvider>
);
}
/**
* List of related pages, with skeleton rows while they load. Renders nothing once we know
* there are no suggestions.
*/
function NotFoundSuggestions(props: { suggestions: RelatedPage[] | null }) {
const { suggestions } = props;
const language = useLanguage();
const loading = suggestions === null;
const hasResults = suggestions != null && suggestions.length > 0;
return loading || hasResults ? (
<div className="-mt-4 flex w-full flex-col gap-2 circular-corners:rounded-b-3xl rounded-corners:rounded-b-xl theme-gradient:border border-tint-subtle bg-tint-subtle theme-muted:bg-tint p-8 pt-10">
<h2 className="font-medium text-tint">{t(language, 'notfound_suggestions_title')}</h2>
<ul className="flex flex-col gap-2">
{loading ? (
<SkeletonList items={RELATED_PAGES_COUNT} className="my-0.75 max-w-md" />
) : (
suggestions.map((suggestion, index) => (
<li
key={suggestion.id}
className="flex grow origin-left animate-blur-in-slow"
style={{ animationDelay: `${index * 25}ms` }}
>
<StyledLink href={suggestion.href} className="flex items-center gap-2">
{suggestion.emoji ? (
<span className="flex size-4 shrink-0 items-center justify-center">
<Emoji
code={suggestion.emoji}
style="text-base leading-none"
/>
</span>
) : (
<Icon
icon={(suggestion.icon as IconName) ?? 'file-lines'}
className="size-4 text-tint-subtle"
/>
)}
<span className="grow truncate">{suggestion.title}</span>
</StyledLink>
</li>
))
)}
</ul>
</div>
) : null;
}
/** Minimal shape of an entry in the `~gitbook/site-index` response. */
type IndexPage = {
id: string;
title: string;
pathname: string;
siteSpaceId: string;
icon?: string;
emoji?: string;
};
/**
* Return the pages whose path is closest to the one that 404'd.
*
* Rather than asking the server (which would mean an extra request per 404), this reuses the
* search index served at `~gitbook/site-index` — already preloaded and CDN-cached on every page —
* so it's a cache hit, not an origin request. The ranking is a lighter, client-side cousin of
* `getSimilarPages` (which the Markdown 404 runs server-side from the full page tree).
*/
async function getRelatedPages(
indexURL: string,
requestedPath: string,
siteSpaceId: string | null
): Promise<RelatedPage[]> {
const response = await fetch(indexURL);
if (!response.ok) {
return [];
}
const { pages } = (await response.json()) as { pages: IndexPage[] };
return pages
.filter((page) => !siteSpaceId || page.siteSpaceId === siteSpaceId)
.map((page) => ({ page, score: scorePathSimilarity(requestedPath, page.pathname) }))
.sort((a, b) => b.score - a.score)
.slice(0, RELATED_PAGES_COUNT)
.map(({ page }) => ({
id: page.id,
title: page.title,
href: page.pathname,
emoji: page.emoji,
icon: page.icon,
}));
}
const normalizePath = (path: string) => path.toLowerCase().replace(/^\/+|\/+$/g, '');
const lastSegment = (path: string) => path.slice(path.lastIndexOf('/') + 1);
const similarity = (left: string, right: string) => {
const max = Math.max(left.length, right.length);
return max === 0 ? 1 : 1 - leven(left, right) / max;
};
/** Path similarity in [0, 1] — full-path closeness, weighted toward the last segment. */
function scorePathSimilarity(input: string, candidate: string): number {
const a = normalizePath(input);
const b = normalizePath(candidate);
if (!a || !b) {
return 0;
}
return similarity(a, b) * 0.6 + similarity(lastSegment(a), lastSegment(b)) * 0.4;
}
@@ -133,11 +133,7 @@ export function SiteSectionTabs(props: {
'data-[motion=from-start]:*:animate-[enterFromLeft_300ms_ease_both] data-[motion=to-end]:*:animate-[exitToRight_300ms_ease_both] data-[motion=to-start]:*:animate-[exitToLeft_300ms_ease_both] motion-safe:data-[motion=from-end]:*:animate-[enterFromRight_300ms_ease_both]',
])}
>
<div
className={tcls(
'max-h-[calc(100vh-8rem)] w-full overflow-y-auto overflow-x-hidden'
)}
>
<div className="max-h-[calc(100vh-8rem)] w-full overflow-y-auto overflow-x-hidden circular-corners:rounded-3xl rounded-corners:rounded-xl">
<SectionGroupTileList
items={structureItem.children}
currentSection={currentSection}
@@ -173,7 +169,9 @@ export function SiteSectionTabs(props: {
>
<NavigationMenu.Viewport
className={tcls(
'relative origin-[center_top] overflow-hidden circular-corners:rounded-3xl rounded-corners:rounded-xl border border-tint bg-tint-base shadow-lg',
// Note: this layer is composited (translateZ) and animated. Chromium fails to paint a clipped composited layer's text
// inside an iframe or `overflow-hidden` ancestor. Clipping is done on the inner content wrapper instead.
'relative origin-[center_top] circular-corners:rounded-3xl rounded-corners:rounded-xl border border-tint bg-tint-base shadow-lg',
'-mt-0.5 h-(--radix-navigation-menu-viewport-height) w-full max-w-full md:w-(--radix-navigation-menu-viewport-width)',
'max-h-[calc(100vh-8rem)] data-[state=closed]:animate-scale-out data-[state=open]:animate-scale-in',
'ease has-[&[data-motion]]:transition-[left,width,height] has-[&[data-motion]]:duration-300'
@@ -71,6 +71,7 @@ export function SpaceLayoutServerContext(props: SpaceLayoutProps) {
<SpaceLayoutContextProvider
basePath={context.linker.toPathInSpace('')}
siteAdaptiveAuthLoginHref={siteAdaptiveAuthLoginHref}
siteIndexURL={context.linker.toPathInSite('~gitbook/site-index')}
>
<AdaptiveVisitorContextProvider
contextId={context.contextId}
@@ -138,7 +139,7 @@ export function SpaceLayout(props: SpaceLayoutProps) {
) : null}
{/* Chat panel shifts content left when open */}
<div className="motion-safe:transition-all motion-safe:duration-300 lg:chat-open:mr-80 xl:chat-open:mr-96">
<div className="motion-safe:transition-all motion-safe:duration-300 lg:chat-open:mr-(--ai-chat-width)">
<div
className={tcls(
'flex',
@@ -5,6 +5,7 @@ import React from 'react';
const SpaceLayoutContext = React.createContext({
basePath: '',
siteAdaptiveAuthLoginHref: null as string | null,
siteIndexURL: '',
});
/**
@@ -14,13 +15,14 @@ export function SpaceLayoutContextProvider(
props: React.PropsWithChildren<{
basePath: string;
siteAdaptiveAuthLoginHref?: string | null;
siteIndexURL: string;
}>
) {
const { basePath, siteAdaptiveAuthLoginHref = null, children } = props;
const { basePath, siteAdaptiveAuthLoginHref = null, siteIndexURL, children } = props;
const value = React.useMemo(
() => ({ basePath, siteAdaptiveAuthLoginHref }),
[basePath, siteAdaptiveAuthLoginHref]
() => ({ basePath, siteAdaptiveAuthLoginHref, siteIndexURL }),
[basePath, siteAdaptiveAuthLoginHref, siteIndexURL]
);
return <SpaceLayoutContext.Provider value={value}>{children}</SpaceLayoutContext.Provider>;
@@ -47,3 +49,14 @@ export function useSiteAdaptiveAuthLoginHref() {
}
return context.siteAdaptiveAuthLoginHref;
}
/**
* Return the URL of the site search index (`~gitbook/site-index`).
*/
export function useSiteIndexURL() {
const context = React.useContext(SpaceLayoutContext);
if (!context) {
throw new Error('SpaceLayoutContext not found');
}
return context.siteIndexURL;
}
@@ -43,7 +43,7 @@ export function PageDocumentItem(props: { page: ClientTOCPageDocument }) {
tag={page.primaryTag ? <Tag tag={page.primaryTag} /> : null}
>
{page.openAPIOperation ? (
<span className="flex h-[1lh] shrink-0 items-center self-center">
<span className="flex h-[1lh] w-9 shrink-0 self-baseline">
<OpenAPIMethodBadge
method={page.openAPIOperation.method}
short
@@ -157,9 +157,14 @@ export function Link(props: LinkProps) {
);
}
// Not sure why yet, but it seems necessary to force prefetch to true
// default behavior doesn't seem to properly use the client router cache.
const _prefetch = prefetch === null || prefetch === undefined ? true : prefetch;
// Not sure why yet, but forcing prefetch to true seems necessary for the
// client router cache to be used properly.
//
// However, we need to disable prefetch for links with query params that
// can trigger server-side side effects, such as persisting visitor claims in a
// cookie or starting the assistant. Automatic RSC prefetch requests can otherwise
// trigger those effects without user intent.
const _prefetch = hasSideEffectQueryParams(href) ? false : (prefetch ?? true);
return (
<NextLink
@@ -175,6 +180,31 @@ export function Link(props: LinkProps) {
);
}
/**
* Whether the given href carries query params that have a server-side side effect when fetched:
* - `visitor.*` params persist unsigned visitor claims into the `gitbook-visitor-public` cookie.
* - `ask` triggers the assistant & `q` the search.
*
* NextLink automatically prefetches links (RSC requests) on hover/viewport, which would fire those
* side effects without any user intent, so such links should not be prefetched.
*/
function hasSideEffectQueryParams(href: string): boolean {
const baseURL = typeof window !== 'undefined' ? window.location.origin : 'http://localhost';
const linkURL = URL.canParse(href) ? new URL(href) : new URL(href, baseURL);
if (linkURL.searchParams.get('ask') !== null || linkURL.searchParams.get('q') !== null) {
return true;
}
for (const key of linkURL.searchParams.keys()) {
if (key.startsWith('visitor.')) {
return true;
}
}
return false;
}
/**
* A box used to contain a link overlay.
* It is used to create a clickable area that can contain other elements.
@@ -54,6 +54,27 @@ export function SkeletonParagraph(props: {
);
}
export function SkeletonList(props: {
items: number;
size?: 'xsmall' | 'small' | 'medium';
start?: number;
className?: ClassValue;
}) {
const { items, size = 'medium', start = 0, className } = props;
const itemHeight = size === 'small' ? 'h-4' : 'h-5';
return Array.from({ length: items }).map((_, index) => (
<li key={index} className={tcls('my-0.5', className)}>
<LoadingItem
className={tcls('origin-left', itemHeight)}
style={{
width: `${(4 - ((index + start) % 4)) * 8 + 62}%`,
animationDelay: `${(index + start) * 0.1}s`,
}}
/>
</li>
));
}
/**
* Placeholder when loading a title.
*/
@@ -0,0 +1,18 @@
import assertNever from 'assert-never';
/**
* Returns the URL to open the page in a LLM with a pre-filled prompt.
*/
export function getURLForLLM(provider: 'chatgpt' | 'claude' | 'cursor', prompt: string) {
const encodedPrompt = encodeURIComponent(prompt);
switch (provider) {
case 'chatgpt':
return `https://chat.openai.com/?q=${encodedPrompt}`;
case 'claude':
return `https://claude.ai/new?q=${encodedPrompt}`;
case 'cursor':
return `https://cursor.com/link/prompt?text=${encodedPrompt}`;
default:
assertNever(provider);
}
}
@@ -1,3 +1,4 @@
export * from './Image';
export * from './dates';
export * from './Favicon';
export * from './getURLForLLM';
@@ -56,6 +56,7 @@ export const ar: TranslationLanguage = {
annotation_button_label: 'فتح التعليق التوضيحي',
code_copied: 'تم النسخ!',
code_copy: 'نسخ',
prompt_copy: 'نسخ الموجّه',
code_block_collapsed: 'عرض كل الأسطر ${1}',
code_block_expanded: 'عرض أقل',
table_of_contents_button_label: 'فتح جدول المحتويات',
@@ -75,7 +76,9 @@ export const ar: TranslationLanguage = {
notfound_adaptive_title: 'الصفحة غير متاحة',
notfound_adaptive: 'قد تكون هذه الصفحة موجودة، لكن قد تحتاج إلى تسجيل الدخول للوصول إليها.',
notfound_adaptive_login: 'تسجيل الدخول',
notfound_adaptive_registration_hint: 'قد يكون التسجيل متاحًا حسب المؤسسة.',
notfound_goto_home: 'الانتقال إلى الصفحة الرئيسية',
notfound_suggestions_title: 'ربما تبحث عن',
unexpected_error_title: 'حدث خطأ',
unexpected_error: 'عذرا، حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى لاحقا.',
unexpected_error_retry: 'حاول مرة أخرى',
@@ -105,6 +108,8 @@ export const ar: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'مساء الخير',
ai_chat_assistant_greeting_night: 'تصبح على خير',
ai_chat_clear_conversation: 'مسح المحادثة',
ai_chat_expand_panel: 'توسيع اللوحة',
ai_chat_collapse_panel: 'طي اللوحة',
ai_chat_thinking: 'جار التفكير...',
ai_chat_working: 'جار العمل...',
ai_chat_exploring: 'جار الاستكشاف...',
@@ -56,6 +56,7 @@ export const bg: TranslationLanguage = {
annotation_button_label: 'Отваряне на бележка',
code_copied: 'Копирано!',
code_copy: 'Копиране',
prompt_copy: 'Копиране на подканата',
code_block_collapsed: 'Показване на всички ${1} реда',
code_block_expanded: 'Показване на по-малко',
table_of_contents_button_label: 'Отваряне на съдържанието',
@@ -76,7 +77,10 @@ export const bg: TranslationLanguage = {
notfound_adaptive:
'Тази страница може да съществува, но може да трябва да влезете, за да я достъпите.',
notfound_adaptive_login: 'Вход',
notfound_adaptive_registration_hint:
'Регистрацията може да е възможна в зависимост от организацията.',
notfound_goto_home: 'Към началната страница',
notfound_suggestions_title: 'Може би търсите',
unexpected_error_title: 'Възникна грешка',
unexpected_error: 'Съжаляваме, възникна неочаквана грешка. Моля, опитайте отново по-късно.',
unexpected_error_retry: 'Опитайте отново',
@@ -107,6 +111,8 @@ export const bg: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Добър вечер',
ai_chat_assistant_greeting_night: 'Лека нощ',
ai_chat_clear_conversation: 'Изчистване на разговора',
ai_chat_expand_panel: 'Разгъване на панела',
ai_chat_collapse_panel: 'Свиване на панела',
ai_chat_thinking: 'Мисля...',
ai_chat_working: 'Работя...',
ai_chat_exploring: 'Проучвам...',
@@ -56,6 +56,7 @@ export const cs: TranslationLanguage = {
annotation_button_label: 'Otevřít anotaci',
code_copied: 'Zkopírováno!',
code_copy: 'Kopírovat',
prompt_copy: 'Kopírovat výzvu',
code_block_collapsed: 'Zobrazit všech ${1} řádků',
code_block_expanded: 'Zobrazit méně',
table_of_contents_button_label: 'Otevřít obsah',
@@ -76,7 +77,9 @@ export const cs: TranslationLanguage = {
notfound_adaptive:
'Tato stránka může existovat, ale pro přístup se možná budete muset přihlásit.',
notfound_adaptive_login: 'Přihlásit se',
notfound_adaptive_registration_hint: 'Registrace může být dostupná v závislosti na organizaci.',
notfound_goto_home: 'Přejít na domovskou stránku',
notfound_suggestions_title: 'Možná hledáte',
unexpected_error_title: 'Došlo k chybě',
unexpected_error: 'Omlouváme se, došlo k neočekávané chybě. Zkuste to prosím znovu později.',
unexpected_error_retry: 'Zkusit znovu',
@@ -107,6 +110,8 @@ export const cs: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Dobrý večer',
ai_chat_assistant_greeting_night: 'Dobrou noc',
ai_chat_clear_conversation: 'Vymazat konverzaci',
ai_chat_expand_panel: 'Rozbalit panel',
ai_chat_collapse_panel: 'Sbalit panel',
ai_chat_thinking: 'Přemýšlím...',
ai_chat_working: 'Pracuji...',
ai_chat_exploring: 'Prozkoumávám...',
@@ -56,6 +56,7 @@ export const da: TranslationLanguage = {
annotation_button_label: 'Åbn kommentar',
code_copied: 'Kopieret!',
code_copy: 'Kopiér',
prompt_copy: 'Kopiér prompten',
code_block_collapsed: 'Vis alle ${1} linjer',
code_block_expanded: 'Vis mindre',
table_of_contents_button_label: 'Åbn indholdsfortegnelse',
@@ -76,7 +77,9 @@ export const da: TranslationLanguage = {
notfound_adaptive:
'Denne side findes muligvis, men du skal muligvis logge ind for at få adgang.',
notfound_adaptive_login: 'Log ind',
notfound_adaptive_registration_hint: 'Registrering kan være mulig afhængigt af organisationen.',
notfound_goto_home: 'Gå til forsiden',
notfound_suggestions_title: 'Du leder måske efter',
unexpected_error_title: 'Der opstod en fejl',
unexpected_error: 'Beklager, der opstod en uventet fejl. Prøv igen senere.',
unexpected_error_retry: 'Prøv igen',
@@ -106,6 +109,8 @@ export const da: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Godaften',
ai_chat_assistant_greeting_night: 'Godnat',
ai_chat_clear_conversation: 'Ryd samtale',
ai_chat_expand_panel: 'Udvid panel',
ai_chat_collapse_panel: 'Formindsk panel',
ai_chat_thinking: 'Tænker...',
ai_chat_working: 'Arbejder...',
ai_chat_exploring: 'Udforsker...',
@@ -55,6 +55,7 @@ export const de = {
annotation_button_label: 'Kommentar öffnen',
code_copied: 'Kopiert!',
code_copy: 'Kopieren',
prompt_copy: 'Prompt kopieren',
code_block_collapsed: 'Alle ${1} Zeilen anzeigen',
code_block_expanded: 'Weniger anzeigen',
table_of_contents_button_label: 'Inhaltsverzeichnis öffnen',
@@ -76,7 +77,10 @@ export const de = {
notfound_adaptive:
'Diese Seite existiert möglicherweise, aber Sie müssen sich eventuell anmelden, um darauf zuzugreifen.',
notfound_adaptive_login: 'Anmelden',
notfound_adaptive_registration_hint:
'Die Registrierung ist je nach Organisation möglicherweise verfügbar.',
notfound_goto_home: 'Zur Startseite',
notfound_suggestions_title: 'Vielleicht suchen Sie nach',
unexpected_error_title: 'Ein Fehler ist aufgetreten',
unexpected_error:
'Entschuldigung, ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.',
@@ -108,6 +112,8 @@ export const de = {
ai_chat_assistant_greeting_evening: 'Guten Abend',
ai_chat_assistant_greeting_night: 'Gute Nacht',
ai_chat_clear_conversation: 'Unterhaltung löschen',
ai_chat_expand_panel: 'Panel erweitern',
ai_chat_collapse_panel: 'Panel reduzieren',
ai_chat_thinking: 'Denke nach...',
ai_chat_working: 'Arbeite...',
ai_chat_exploring: 'Erkunde...',
@@ -56,6 +56,7 @@ export const el: TranslationLanguage = {
annotation_button_label: 'Άνοιγμα σχολίου',
code_copied: 'Αντιγράφηκε!',
code_copy: 'Αντιγραφή',
prompt_copy: 'Αντιγραφή προτροπής',
code_block_collapsed: 'Εμφάνιση όλων των ${1} γραμμών',
code_block_expanded: 'Εμφάνιση λιγότερων',
table_of_contents_button_label: 'Άνοιγμα πίνακα περιεχομένων',
@@ -76,7 +77,10 @@ export const el: TranslationLanguage = {
notfound_adaptive:
'Αυτή η σελίδα μπορεί να υπάρχει, αλλά ίσως χρειαστεί να συνδεθείτε για πρόσβαση.',
notfound_adaptive_login: 'Σύνδεση',
notfound_adaptive_registration_hint:
'Η εγγραφή ενδέχεται να είναι διαθέσιμη ανάλογα με τον οργανισμό.',
notfound_goto_home: 'Μετάβαση στην αρχική σελίδα',
notfound_suggestions_title: 'Ίσως αναζητάτε',
unexpected_error_title: 'Παρουσιάστηκε σφάλμα',
unexpected_error:
'Λυπούμαστε, παρουσιάστηκε ένα μη αναμενόμενο σφάλμα. Δοκιμάστε ξανά αργότερα.',
@@ -108,6 +112,8 @@ export const el: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Καλό βράδυ',
ai_chat_assistant_greeting_night: 'Καληνύχτα',
ai_chat_clear_conversation: 'Εκκαθάριση συνομιλίας',
ai_chat_expand_panel: 'Ανάπτυξη πίνακα',
ai_chat_collapse_panel: 'Σύμπτυξη πίνακα',
ai_chat_thinking: 'Σκέφτομαι...',
ai_chat_working: 'Εργάζομαι...',
ai_chat_exploring: 'Εξερευνώ...',
+7 -1
View File
@@ -54,6 +54,7 @@ export const en = {
annotation_button_label: 'Open annotation',
code_copied: 'Copied!',
code_copy: 'Copy',
prompt_copy: 'Copy prompt',
code_block_collapsed: 'Show all ${1} lines',
code_block_expanded: 'Show less',
table_of_contents_button_label: 'Open table of contents',
@@ -69,11 +70,14 @@ export const en = {
edit: 'Edit',
notfound_title: 'Page not found',
notfound_link: 'This link points to a page that has been removed or no longer exists.',
notfound: "The page you are looking for doesn't exist.",
notfound: "The page you're looking for doesn't exist.",
notfound_adaptive_title: 'Page unavailable',
notfound_adaptive: 'This page may exist, but you may need to log in to access it.',
notfound_adaptive_login: 'Log in',
notfound_adaptive_registration_hint:
'Registration may be available depending on the organization.',
notfound_goto_home: 'Go to homepage',
notfound_suggestions_title: 'You might be looking for',
unexpected_error_title: 'An error occurred',
unexpected_error: 'Sorry, an unexpected error has occurred. Please try again later.',
unexpected_error_retry: 'Try again',
@@ -103,6 +107,8 @@ export const en = {
ai_chat_assistant_greeting_evening: 'Good evening',
ai_chat_assistant_greeting_night: 'Good night',
ai_chat_clear_conversation: 'Clear conversation',
ai_chat_expand_panel: 'Expand panel',
ai_chat_collapse_panel: 'Collapse panel',
ai_chat_thinking: 'Thinking…',
ai_chat_working: 'Working…',
ai_chat_exploring: 'Exploring…',
@@ -57,6 +57,7 @@ export const es: TranslationLanguage = {
annotation_button_label: 'Abrir anotación',
code_copied: '¡Copiado!',
code_copy: 'Copiar',
prompt_copy: 'Copiar prompt',
code_block_collapsed: 'Mostrar las ${1} líneas',
code_block_expanded: 'Mostrar menos',
table_of_contents_button_label: 'Abrir índice de contenidos',
@@ -77,7 +78,10 @@ export const es: TranslationLanguage = {
notfound_adaptive:
'Esta página puede existir, pero es posible que debas iniciar sesión para acceder.',
notfound_adaptive_login: 'Iniciar sesión',
notfound_adaptive_registration_hint:
'El registro puede estar disponible según la organización.',
notfound_goto_home: 'Ir al inicio',
notfound_suggestions_title: 'Quizás estés buscando',
unexpected_error_title: 'Ocurrió un error',
unexpected_error:
'Lo sentimos, ha ocurrido un error inesperado. Por favor, intenta de nuevo más tarde.',
@@ -109,6 +113,8 @@ export const es: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Buenas tardes',
ai_chat_assistant_greeting_night: 'Buenas noches',
ai_chat_clear_conversation: 'Limpiar conversación',
ai_chat_expand_panel: 'Expandir panel',
ai_chat_collapse_panel: 'Contraer panel',
ai_chat_thinking: 'Pensando...',
ai_chat_working: 'Trabajando...',
ai_chat_exploring: 'Explorando...',
@@ -56,6 +56,7 @@ export const et: TranslationLanguage = {
annotation_button_label: 'Ava märkus',
code_copied: 'Kopeeritud!',
code_copy: 'Kopeeri',
prompt_copy: 'Kopeeri viip',
code_block_collapsed: 'Kuva kõik ${1} rida',
code_block_expanded: 'Kuva vähem',
table_of_contents_button_label: 'Ava sisukord',
@@ -75,7 +76,10 @@ export const et: TranslationLanguage = {
notfound_adaptive_title: 'Leht pole saadaval',
notfound_adaptive: 'See leht võib olemas olla, kuid juurdepääsuks võib olla vaja sisse logida.',
notfound_adaptive_login: 'Logi sisse',
notfound_adaptive_registration_hint:
'Registreerimine võib olla saadaval olenevalt organisatsioonist.',
notfound_goto_home: 'Mine avalehele',
notfound_suggestions_title: 'Võib-olla otsite',
unexpected_error_title: 'Ilmnes tõrge',
unexpected_error: 'Vabandust, ilmnes ootamatu tõrge. Palun proovige hiljem uuesti.',
unexpected_error_retry: 'Proovi uuesti',
@@ -105,6 +109,8 @@ export const et: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Tere õhtust',
ai_chat_assistant_greeting_night: 'Head ööd',
ai_chat_clear_conversation: 'Tühjenda vestlus',
ai_chat_expand_panel: 'Laienda paneeli',
ai_chat_collapse_panel: 'Ahenda paneeli',
ai_chat_thinking: 'Mõtlen...',
ai_chat_working: 'Töötan...',
ai_chat_exploring: 'Uurin...',
@@ -56,6 +56,7 @@ export const fi: TranslationLanguage = {
annotation_button_label: 'Avaa huomautus',
code_copied: 'Kopioitu!',
code_copy: 'Kopioi',
prompt_copy: 'Kopioi kehote',
code_block_collapsed: 'Näytä kaikki ${1} riviä',
code_block_expanded: 'Näytä vähemmän',
table_of_contents_button_label: 'Avaa sisällysluettelo',
@@ -76,7 +77,10 @@ export const fi: TranslationLanguage = {
notfound_adaptive:
'Tämä sivu voi olla olemassa, mutta sen käyttö voi edellyttää kirjautumista.',
notfound_adaptive_login: 'Kirjaudu sisään',
notfound_adaptive_registration_hint:
'Rekisteröityminen voi olla mahdollista organisaatiosta riippuen.',
notfound_goto_home: 'Siirry etusivulle',
notfound_suggestions_title: 'Saatat etsiä',
unexpected_error_title: 'Tapahtui virhe',
unexpected_error: 'Valitettavasti tapahtui odottamaton virhe. Yritä myöhemmin uudelleen.',
unexpected_error_retry: 'Yritä uudelleen',
@@ -106,6 +110,8 @@ export const fi: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Hyvää iltaa',
ai_chat_assistant_greeting_night: 'Hyvää yötä',
ai_chat_clear_conversation: 'Tyhjennä keskustelu',
ai_chat_expand_panel: 'Laajenna paneeli',
ai_chat_collapse_panel: 'Pienennä paneeli',
ai_chat_thinking: 'Ajatellaan...',
ai_chat_working: 'Työskennellään...',
ai_chat_exploring: 'Tutkitaan...',
@@ -54,6 +54,7 @@ export const fr = {
annotation_button_label: 'Afficher lannotation',
code_copied: 'Copié !',
code_copy: 'Copier',
prompt_copy: 'Copier linvite',
code_block_collapsed: 'Afficher les ${1} lignes',
code_block_expanded: 'Afficher moins',
table_of_contents_button_label: 'Afficher le sommaire',
@@ -74,7 +75,9 @@ export const fr = {
notfound_adaptive:
'Cette page existe peut-être, mais vous devez peut-être vous connecter pour y accéder.',
notfound_adaptive_login: 'Se connecter',
notfound_adaptive_registration_hint: "L'inscription peut être disponible selon l'organisation.",
notfound_goto_home: "Aller à l'accueil",
notfound_suggestions_title: 'Vous cherchez peut-être',
unexpected_error_title: 'Erreur inattendue',
unexpected_error: 'Désolé, une erreur est survenue. Veuillez réessayer plus tard.',
unexpected_error_retry: 'Réessayer',
@@ -104,6 +107,8 @@ export const fr = {
ai_chat_assistant_greeting_evening: 'Bonsoir',
ai_chat_assistant_greeting_night: 'Bonne nuit',
ai_chat_clear_conversation: 'Réinitialiser la conversation',
ai_chat_expand_panel: 'Agrandir le panneau',
ai_chat_collapse_panel: 'Réduire le panneau',
ai_chat_thinking: 'Réflexion en cours…',
ai_chat_working: 'Traitement en cours…',
ai_chat_exploring: 'Exploration en cours…',
@@ -56,6 +56,7 @@ export const he: TranslationLanguage = {
annotation_button_label: 'פתיחת הערה',
code_copied: 'הועתק!',
code_copy: 'העתקה',
prompt_copy: 'העתקת ההנחיה',
code_block_collapsed: 'הצג את כל ${1} השורות',
code_block_expanded: 'הצג פחות',
table_of_contents_button_label: 'פתיחת תוכן העניינים',
@@ -75,7 +76,9 @@ export const he: TranslationLanguage = {
notfound_adaptive_title: 'הדף אינו זמין',
notfound_adaptive: 'ייתכן שהדף קיים, אך ייתכן שעליך להתחבר כדי לגשת אליו.',
notfound_adaptive_login: 'התחברות',
notfound_adaptive_registration_hint: 'ייתכן שההרשמה זמינה בהתאם לארגון.',
notfound_goto_home: 'מעבר לדף הבית',
notfound_suggestions_title: 'אולי אתה מחפש',
unexpected_error_title: 'אירעה שגיאה',
unexpected_error: 'מצטערים, אירעה שגיאה בלתי צפויה. נסה שוב מאוחר יותר.',
unexpected_error_retry: 'נסה שוב',
@@ -105,6 +108,8 @@ export const he: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'ערב טוב',
ai_chat_assistant_greeting_night: 'לילה טוב',
ai_chat_clear_conversation: 'ניקוי השיחה',
ai_chat_expand_panel: 'הרחב פאנל',
ai_chat_collapse_panel: 'כווץ פאנל',
ai_chat_thinking: 'חושב...',
ai_chat_working: 'עובד...',
ai_chat_exploring: 'בודק...',
@@ -56,6 +56,7 @@ export const hi: TranslationLanguage = {
annotation_button_label: 'टिप्पणी खोलें',
code_copied: 'कॉपी हो गया!',
code_copy: 'कॉपी करें',
prompt_copy: 'प्रॉम्प्ट कॉपी करें',
code_block_collapsed: 'सभी ${1} पंक्तियां दिखाएं',
code_block_expanded: 'कम दिखाएं',
table_of_contents_button_label: 'सामग्री सूची खोलें',
@@ -75,7 +76,9 @@ export const hi: TranslationLanguage = {
notfound_adaptive_title: 'पृष्ठ उपलब्ध नहीं',
notfound_adaptive: 'यह पृष्ठ मौजूद हो सकता है, लेकिन इसे देखने के लिए आपको लॉग इन करना पड़ सकता है।',
notfound_adaptive_login: 'लॉग इन करें',
notfound_adaptive_registration_hint: 'संगठन के आधार पर पंजीकरण उपलब्ध हो सकता है।',
notfound_goto_home: 'मुखपृष्ठ पर जाएं',
notfound_suggestions_title: 'शायद आप यह ढूँढ रहे हैं',
unexpected_error_title: 'त्रुटि हुई',
unexpected_error: 'क्षमा करें, एक अनपेक्षित त्रुटि हुई। कृपया बाद में फिर कोशिश करें।',
unexpected_error_retry: 'फिर कोशिश करें',
@@ -105,6 +108,8 @@ export const hi: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'शुभ संध्या',
ai_chat_assistant_greeting_night: 'शुभ रात्रि',
ai_chat_clear_conversation: 'बातचीत साफ करें',
ai_chat_expand_panel: 'पैनल का विस्तार करें',
ai_chat_collapse_panel: 'पैनल को छोटा करें',
ai_chat_thinking: 'सोच रहा है...',
ai_chat_working: 'काम कर रहा है...',
ai_chat_exploring: 'खोज रहा है...',
@@ -56,6 +56,7 @@ export const hr: TranslationLanguage = {
annotation_button_label: 'Otvori bilješku',
code_copied: 'Kopirano!',
code_copy: 'Kopiraj',
prompt_copy: 'Kopiraj uputu',
code_block_collapsed: 'Prikaži svih ${1} redaka',
code_block_expanded: 'Prikaži manje',
table_of_contents_button_label: 'Otvori sadržaj',
@@ -76,7 +77,9 @@ export const hr: TranslationLanguage = {
notfound_adaptive:
'Ova stranica možda postoji, ali možda se morate prijaviti da biste joj pristupili.',
notfound_adaptive_login: 'Prijava',
notfound_adaptive_registration_hint: 'Registracija može biti dostupna ovisno o organizaciji.',
notfound_goto_home: 'Idi na početnu stranicu',
notfound_suggestions_title: 'Možda tražite',
unexpected_error_title: 'Došlo je do pogreške',
unexpected_error: 'Žao nam je, došlo je do neočekivane pogreške. Pokušajte ponovno kasnije.',
unexpected_error_retry: 'Pokušaj ponovno',
@@ -107,6 +110,8 @@ export const hr: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Dobra večer',
ai_chat_assistant_greeting_night: 'Laku noć',
ai_chat_clear_conversation: 'Očisti razgovor',
ai_chat_expand_panel: 'Proširi ploču',
ai_chat_collapse_panel: 'Sažmi ploču',
ai_chat_thinking: 'Razmišljam...',
ai_chat_working: 'Radim...',
ai_chat_exploring: 'Istražujem...',
@@ -56,6 +56,7 @@ export const hu: TranslationLanguage = {
annotation_button_label: 'Jegyzet megnyitása',
code_copied: 'Másolva!',
code_copy: 'Másolás',
prompt_copy: 'Prompt másolása',
code_block_collapsed: 'Mind a(z) ${1} sor megjelenítése',
code_block_expanded: 'Kevesebb megjelenítése',
table_of_contents_button_label: 'Tartalomjegyzék megnyitása',
@@ -75,7 +76,9 @@ export const hu: TranslationLanguage = {
notfound_adaptive_title: 'Az oldal nem érhető el',
notfound_adaptive: 'Ez az oldal létezhet, de a hozzáféréshez lehet, hogy be kell jelentkeznie.',
notfound_adaptive_login: 'Bejelentkezés',
notfound_adaptive_registration_hint: 'A regisztráció a szervezettől függően elérhető lehet.',
notfound_goto_home: 'Ugrás a kezdőlapra',
notfound_suggestions_title: 'Talán ezt keresi',
unexpected_error_title: 'Hiba történt',
unexpected_error: 'Sajnáljuk, váratlan hiba történt. Kérjük, próbálja újra később.',
unexpected_error_retry: 'Próbálja újra',
@@ -106,6 +109,8 @@ export const hu: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Jó estét',
ai_chat_assistant_greeting_night: 'Jó éjszakát',
ai_chat_clear_conversation: 'Beszélgetés törlése',
ai_chat_expand_panel: 'Panel kibontása',
ai_chat_collapse_panel: 'Panel összecsukása',
ai_chat_thinking: 'Gondolkodom...',
ai_chat_working: 'Dolgozom...',
ai_chat_exploring: 'Felfedezés...',
@@ -56,6 +56,7 @@ export const id: TranslationLanguage = {
annotation_button_label: 'Buka anotasi',
code_copied: 'Disalin!',
code_copy: 'Salin',
prompt_copy: 'Salin prompt',
code_block_collapsed: 'Tampilkan semua ${1} baris',
code_block_expanded: 'Tampilkan lebih sedikit',
table_of_contents_button_label: 'Buka daftar isi',
@@ -76,7 +77,9 @@ export const id: TranslationLanguage = {
notfound_adaptive:
'Halaman ini mungkin ada, tetapi Anda mungkin perlu masuk untuk mengaksesnya.',
notfound_adaptive_login: 'Masuk',
notfound_adaptive_registration_hint: 'Pendaftaran mungkin tersedia tergantung pada organisasi.',
notfound_goto_home: 'Buka beranda',
notfound_suggestions_title: 'Mungkin Anda mencari',
unexpected_error_title: 'Terjadi kesalahan',
unexpected_error: 'Maaf, terjadi kesalahan yang tidak terduga. Silakan coba lagi nanti.',
unexpected_error_retry: 'Coba lagi',
@@ -106,6 +109,8 @@ export const id: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Selamat malam',
ai_chat_assistant_greeting_night: 'Selamat malam',
ai_chat_clear_conversation: 'Bersihkan percakapan',
ai_chat_expand_panel: 'Perluas panel',
ai_chat_collapse_panel: 'Ciutkan panel',
ai_chat_thinking: 'Berpikir...',
ai_chat_working: 'Bekerja...',
ai_chat_exploring: 'Menjelajahi...',
@@ -57,6 +57,7 @@ export const it: TranslationLanguage = {
annotation_button_label: 'Apri annotazione',
code_copied: 'Copiato!',
code_copy: 'Copia',
prompt_copy: 'Copia il prompt',
code_block_collapsed: 'Mostra tutte le ${1} righe',
code_block_expanded: 'Mostra meno',
table_of_contents_button_label: 'Apri indice dei contenuti',
@@ -77,7 +78,10 @@ export const it: TranslationLanguage = {
notfound_adaptive:
"Questa pagina potrebbe esistere, ma potrebbe essere necessario effettuare l'accesso per visualizzarla.",
notfound_adaptive_login: 'Accedi',
notfound_adaptive_registration_hint:
"La registrazione potrebbe essere disponibile a seconda dell'organizzazione.",
notfound_goto_home: 'Vai alla home',
notfound_suggestions_title: 'Forse stai cercando',
unexpected_error_title: 'Si è verificato un errore',
unexpected_error: 'Spiacenti, si è verificato un errore imprevisto. Riprova più tardi.',
unexpected_error_retry: 'Riprova',
@@ -108,6 +112,8 @@ export const it: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Buonasera',
ai_chat_assistant_greeting_night: 'Buonanotte',
ai_chat_clear_conversation: 'Cancella conversazione',
ai_chat_expand_panel: 'Espandi pannello',
ai_chat_collapse_panel: 'Comprimi pannello',
ai_chat_thinking: 'Sto pensando...',
ai_chat_working: 'Sto lavorando...',
ai_chat_exploring: 'Esplorando...',
@@ -57,6 +57,7 @@ export const ja: TranslationLanguage = {
annotation_button_label: '注釈を開く',
code_copied: 'コピーしました!',
code_copy: 'コピー',
prompt_copy: 'プロンプトをコピー',
code_block_collapsed: 'すべての ${1} 行を表示',
code_block_expanded: '折りたたむ',
table_of_contents_button_label: '目次を開く',
@@ -77,7 +78,9 @@ export const ja: TranslationLanguage = {
notfound_adaptive:
'このページは存在する可能性がありますが、アクセスするにはログインが必要な場合があります。',
notfound_adaptive_login: 'ログイン',
notfound_adaptive_registration_hint: '組織によっては登録が可能な場合があります。',
notfound_goto_home: 'ホームへ移動',
notfound_suggestions_title: 'お探しのページはこちらですか',
unexpected_error_title: 'エラーが発生しました',
unexpected_error: '申し訳ありません、予期せぬエラーが発生しました。もう一度お試しください。',
unexpected_error_retry: '再試行',
@@ -107,6 +110,8 @@ export const ja: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'こんばんは',
ai_chat_assistant_greeting_night: 'おやすみなさい',
ai_chat_clear_conversation: '会話をクリア',
ai_chat_expand_panel: 'パネルを広げる',
ai_chat_collapse_panel: 'パネルを狭める',
ai_chat_thinking: '考え中...',
ai_chat_working: '作業中...',
ai_chat_exploring: '探索中...',
@@ -57,6 +57,7 @@ export const ko: TranslationLanguage = {
annotation_button_label: '주석 열기',
code_copied: '복사됨!',
code_copy: '복사',
prompt_copy: '프롬프트 복사',
code_block_collapsed: '${1}줄 모두 보기',
code_block_expanded: '간단히 보기',
table_of_contents_button_label: '목차 열기',
@@ -76,7 +77,9 @@ export const ko: TranslationLanguage = {
notfound_adaptive_title: '페이지에 접근할 수 없음',
notfound_adaptive: '이 페이지는 존재할 수 있지만, 접근하려면 로그인해야 할 수 있습니다.',
notfound_adaptive_login: '로그인',
notfound_adaptive_registration_hint: '조직에 따라 등록이 가능할 수 있습니다.',
notfound_goto_home: '홈으로 이동',
notfound_suggestions_title: '찾고 계신 내용일 수 있어요',
unexpected_error_title: '오류가 발생했습니다',
unexpected_error: '죄송합니다. 예상치 못한 오류가 발생했습니다. 나중에 다시 시도해 주세요.',
unexpected_error_retry: '다시 시도',
@@ -106,6 +109,8 @@ export const ko: TranslationLanguage = {
ai_chat_assistant_greeting_evening: '좋은 저녁입니다',
ai_chat_assistant_greeting_night: '안녕히 주무세요',
ai_chat_clear_conversation: '대화 지우기',
ai_chat_expand_panel: '패널 확장',
ai_chat_collapse_panel: '패널 축소',
ai_chat_thinking: '생각 중...',
ai_chat_working: '작업 중...',
ai_chat_exploring: '탐색 중...',
@@ -56,6 +56,7 @@ export const lt: TranslationLanguage = {
annotation_button_label: 'Atidaryti pastabą',
code_copied: 'Nukopijuota!',
code_copy: 'Kopijuoti',
prompt_copy: 'Kopijuoti raginimą',
code_block_collapsed: 'Rodyti visas ${1} eilutes',
code_block_expanded: 'Rodyti mažiau',
table_of_contents_button_label: 'Atidaryti turinį',
@@ -76,7 +77,10 @@ export const lt: TranslationLanguage = {
notfound_adaptive:
'Šis puslapis gali egzistuoti, bet norint jį pasiekti gali reikėti prisijungti.',
notfound_adaptive_login: 'Prisijungti',
notfound_adaptive_registration_hint:
'Registracija gali būti galima priklausomai nuo organizacijos.',
notfound_goto_home: 'Eiti į pagrindinį puslapį',
notfound_suggestions_title: 'Galbūt ieškote',
unexpected_error_title: 'Įvyko klaida',
unexpected_error: 'Atsiprašome, įvyko netikėta klaida. Bandykite dar kartą vėliau.',
unexpected_error_retry: 'Bandyti dar kartą',
@@ -106,6 +110,8 @@ export const lt: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Labas vakaras',
ai_chat_assistant_greeting_night: 'Labanakt',
ai_chat_clear_conversation: 'Išvalyti pokalbį',
ai_chat_expand_panel: 'Išskleisti skydelį',
ai_chat_collapse_panel: 'Sutraukti skydelį',
ai_chat_thinking: 'Galvoju...',
ai_chat_working: 'Dirbu...',
ai_chat_exploring: 'Tyrinėju...',
@@ -56,6 +56,7 @@ export const lv: TranslationLanguage = {
annotation_button_label: 'Atvērt anotāciju',
code_copied: 'Nokopēts!',
code_copy: 'Kopēt',
prompt_copy: 'Kopēt uzvedni',
code_block_collapsed: 'Rādīt visas ${1} rindas',
code_block_expanded: 'Rādīt mazāk',
table_of_contents_button_label: 'Atvērt satura rādītāju',
@@ -75,7 +76,9 @@ export const lv: TranslationLanguage = {
notfound_adaptive_title: 'Lapa nav pieejama',
notfound_adaptive: 'Šī lapa var pastāvēt, bet, iespējams, jums jāpierakstās, lai tai piekļūtu.',
notfound_adaptive_login: 'Pierakstīties',
notfound_adaptive_registration_hint: 'Reģistrācija var būt pieejama atkarībā no organizācijas.',
notfound_goto_home: 'Doties uz sākumlapu',
notfound_suggestions_title: 'Iespējams, meklējat',
unexpected_error_title: 'Radās kļūda',
unexpected_error: 'Diemžēl radās neparedzēta kļūda. Lūdzu, mēģiniet vēlāk vēlreiz.',
unexpected_error_retry: 'Mēģināt vēlreiz',
@@ -105,6 +108,8 @@ export const lv: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Labvakar',
ai_chat_assistant_greeting_night: 'Ar labu nakti',
ai_chat_clear_conversation: 'Notīrīt sarunu',
ai_chat_expand_panel: 'Izvērst paneli',
ai_chat_collapse_panel: 'Sakļaut paneli',
ai_chat_thinking: 'Domāju...',
ai_chat_working: 'Strādāju...',
ai_chat_exploring: 'Izpētu...',
@@ -56,6 +56,7 @@ export const ms: TranslationLanguage = {
annotation_button_label: 'Buka anotasi',
code_copied: 'Disalin!',
code_copy: 'Salin',
prompt_copy: 'Salin gesaan',
code_block_collapsed: 'Tunjukkan semua ${1} baris',
code_block_expanded: 'Tunjukkan kurang',
table_of_contents_button_label: 'Buka jadual kandungan',
@@ -76,7 +77,9 @@ export const ms: TranslationLanguage = {
notfound_adaptive:
'Halaman ini mungkin wujud, tetapi anda mungkin perlu log masuk untuk mengaksesnya.',
notfound_adaptive_login: 'Log masuk',
notfound_adaptive_registration_hint: 'Pendaftaran mungkin tersedia bergantung pada organisasi.',
notfound_goto_home: 'Pergi ke halaman utama',
notfound_suggestions_title: 'Mungkin anda mencari',
unexpected_error_title: 'Ralat berlaku',
unexpected_error: 'Maaf, ralat yang tidak dijangka berlaku. Sila cuba lagi kemudian.',
unexpected_error_retry: 'Cuba lagi',
@@ -106,6 +109,8 @@ export const ms: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Selamat petang',
ai_chat_assistant_greeting_night: 'Selamat malam',
ai_chat_clear_conversation: 'Kosongkan perbualan',
ai_chat_expand_panel: 'Kembangkan panel',
ai_chat_collapse_panel: 'Kecilkan panel',
ai_chat_thinking: 'Berfikir...',
ai_chat_working: 'Bekerja...',
ai_chat_exploring: 'Meneroka...',
+7 -1
View File
@@ -57,6 +57,7 @@ export const nl: TranslationLanguage = {
annotation_button_label: 'Annotatie openen',
code_copied: 'Gekopieerd!',
code_copy: 'Kopiëren',
prompt_copy: 'Prompt kopiëren',
code_block_collapsed: 'Toon alle ${1} regels',
code_block_expanded: 'Toon minder',
table_of_contents_button_label: 'Open inhoudsopgave',
@@ -77,7 +78,10 @@ export const nl: TranslationLanguage = {
notfound_adaptive:
'Deze pagina bestaat mogelijk, maar je moet mogelijk inloggen om toegang te krijgen.',
notfound_adaptive_login: 'Inloggen',
notfound_goto_home: 'Ga naar home',
notfound_adaptive_registration_hint:
'Registratie is mogelijk beschikbaar, afhankelijk van de organisatie.',
notfound_goto_home: 'Naar startpagina',
notfound_suggestions_title: 'Misschien zoek je',
unexpected_error_title: 'Er is een fout opgetreden',
unexpected_error: 'Sorry, er is een onverwachte fout opgetreden. Probeer het later opnieuw.',
unexpected_error_retry: 'Opnieuw proberen',
@@ -107,6 +111,8 @@ export const nl: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Goedenavond',
ai_chat_assistant_greeting_night: 'Goedenacht',
ai_chat_clear_conversation: 'Gesprek wissen',
ai_chat_expand_panel: 'Paneel uitvouwen',
ai_chat_collapse_panel: 'Paneel samenvouwen',
ai_chat_thinking: 'Denkt na...',
ai_chat_working: 'Werkt...',
ai_chat_exploring: 'Verkent...',
@@ -57,6 +57,7 @@ export const no: TranslationLanguage = {
annotation_button_label: 'Åpne merknad',
code_copied: 'Kopiert!',
code_copy: 'Kopier',
prompt_copy: 'Kopier ledeteksten',
code_block_collapsed: 'Vis alle ${1} linjer',
code_block_expanded: 'Vis færre',
table_of_contents_button_label: 'Åpne innholdsfortegnelse',
@@ -77,7 +78,10 @@ export const no: TranslationLanguage = {
notfound_adaptive_title: 'Siden er ikke tilgjengelig',
notfound_adaptive: 'Denne siden kan finnes, men du må kanskje logge inn for å få tilgang.',
notfound_adaptive_login: 'Logg inn',
notfound_adaptive_registration_hint:
'Registrering kan være tilgjengelig avhengig av organisasjonen.',
notfound_goto_home: 'Gå til forsiden',
notfound_suggestions_title: 'Kanskje du leter etter',
unexpected_error_title: 'En feil oppstod',
unexpected_error: 'Beklager, en uventet feil har oppstått. Vennligst prøv igjen senere.',
unexpected_error_retry: 'Prøv igjen',
@@ -107,6 +111,8 @@ export const no: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'God kveld',
ai_chat_assistant_greeting_night: 'God natt',
ai_chat_clear_conversation: 'Tøm samtale',
ai_chat_expand_panel: 'Utvid panel',
ai_chat_collapse_panel: 'Reduser panel',
ai_chat_thinking: 'Tenker...',
ai_chat_working: 'Arbeider...',
ai_chat_exploring: 'Utforsker...',
@@ -56,6 +56,7 @@ export const pl: TranslationLanguage = {
annotation_button_label: 'Otwórz adnotację',
code_copied: 'Skopiowano!',
code_copy: 'Kopiuj',
prompt_copy: 'Kopiuj prompt',
code_block_collapsed: 'Pokaż wszystkie ${1} wierszy',
code_block_expanded: 'Pokaż mniej',
table_of_contents_button_label: 'Otwórz spis treści',
@@ -75,7 +76,10 @@ export const pl: TranslationLanguage = {
notfound_adaptive_title: 'Strona niedostępna',
notfound_adaptive: 'Ta strona może istnieć, ale dostęp do niej może wymagać zalogowania.',
notfound_adaptive_login: 'Zaloguj się',
notfound_adaptive_registration_hint:
'Rejestracja może być dostępna w zależności od organizacji.',
notfound_goto_home: 'Przejdź do strony głównej',
notfound_suggestions_title: 'Być może szukasz',
unexpected_error_title: 'Wystąpił błąd',
unexpected_error: 'Przepraszamy, wystąpił nieoczekiwany błąd. Spróbuj ponownie później.',
unexpected_error_retry: 'Spróbuj ponownie',
@@ -106,6 +110,8 @@ export const pl: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Dobry wieczór',
ai_chat_assistant_greeting_night: 'Dobranoc',
ai_chat_clear_conversation: 'Wyczyść rozmowę',
ai_chat_expand_panel: 'Rozwiń panel',
ai_chat_collapse_panel: 'Zwiń panel',
ai_chat_thinking: 'Myślę...',
ai_chat_working: 'Pracuję...',
ai_chat_exploring: 'Sprawdzam...',
@@ -55,6 +55,7 @@ export const pt_br = {
annotation_button_label: 'Abrir anotação',
code_copied: 'Copiado!',
code_copy: 'Copiar',
prompt_copy: 'Copiar prompt',
code_block_collapsed: 'Mostrar todas as ${1} linhas',
code_block_expanded: 'Mostrar menos',
table_of_contents_button_label: 'Abrir o índice',
@@ -75,7 +76,10 @@ export const pt_br = {
notfound_adaptive:
'Esta página pode existir, mas talvez você precise fazer login para acessá-la.',
notfound_adaptive_login: 'Fazer login',
notfound_adaptive_registration_hint:
'O registro pode estar disponível dependendo da organização.',
notfound_goto_home: 'Ir para a página inicial',
notfound_suggestions_title: 'Você pode estar procurando por',
unexpected_error_title: 'Aconteceu um erro',
unexpected_error:
'Desculpe, aconteceu um erro inesperado. Por favor, tente novamente mais tarde.',
@@ -107,6 +111,8 @@ export const pt_br = {
ai_chat_assistant_greeting_evening: 'Boa tarde',
ai_chat_assistant_greeting_night: 'Boa noite',
ai_chat_clear_conversation: 'Limpar conversa',
ai_chat_expand_panel: 'Expandir painel',
ai_chat_collapse_panel: 'Recolher painel',
ai_chat_thinking: 'Pensando...',
ai_chat_working: 'Trabalhando...',
ai_chat_exploring: 'Explorando...',
@@ -56,6 +56,7 @@ export const pt: TranslationLanguage = {
annotation_button_label: 'Abrir anotação',
code_copied: 'Copiado!',
code_copy: 'Copiar',
prompt_copy: 'Copiar prompt',
code_block_collapsed: 'Mostrar todas as ${1} linhas',
code_block_expanded: 'Mostrar menos',
table_of_contents_button_label: 'Abrir índice',
@@ -76,7 +77,10 @@ export const pt: TranslationLanguage = {
notfound_adaptive:
'Esta página pode existir, mas talvez tenha de iniciar sessão para lhe aceder.',
notfound_adaptive_login: 'Iniciar sessão',
notfound_adaptive_registration_hint:
'O registo pode estar disponível dependendo da organização.',
notfound_goto_home: 'Ir para a página inicial',
notfound_suggestions_title: 'Talvez esteja à procura de',
unexpected_error_title: 'Ocorreu um erro',
unexpected_error: 'Lamentamos, ocorreu um erro inesperado. Tente novamente mais tarde.',
unexpected_error_retry: 'Tentar novamente',
@@ -106,6 +110,8 @@ export const pt: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Boa noite',
ai_chat_assistant_greeting_night: 'Boa noite',
ai_chat_clear_conversation: 'Limpar conversa',
ai_chat_expand_panel: 'Expandir painel',
ai_chat_collapse_panel: 'Recolher painel',
ai_chat_thinking: 'A pensar...',
ai_chat_working: 'A trabalhar...',
ai_chat_exploring: 'A explorar...',
@@ -56,6 +56,7 @@ export const ro: TranslationLanguage = {
annotation_button_label: 'Deschide adnotarea',
code_copied: 'Copiat!',
code_copy: 'Copiază',
prompt_copy: 'Copiază promptul',
code_block_collapsed: 'Afișează toate cele ${1} linii',
code_block_expanded: 'Afișează mai puțin',
table_of_contents_button_label: 'Deschide cuprinsul',
@@ -76,7 +77,10 @@ export const ro: TranslationLanguage = {
notfound_adaptive:
'Această pagină poate exista, dar este posibil să trebuiască să te autentifici pentru a o accesa.',
notfound_adaptive_login: 'Autentificare',
notfound_adaptive_registration_hint:
'Înregistrarea poate fi disponibilă în funcție de organizație.',
notfound_goto_home: 'Mergi la pagina principală',
notfound_suggestions_title: 'Poate cauți',
unexpected_error_title: 'A apărut o eroare',
unexpected_error:
'Ne pare rău, a apărut o eroare neașteptată. Te rugăm să încerci din nou mai târziu.',
@@ -108,6 +112,8 @@ export const ro: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Bună seara',
ai_chat_assistant_greeting_night: 'Noapte bună',
ai_chat_clear_conversation: 'Șterge conversația',
ai_chat_expand_panel: 'Extinde panoul',
ai_chat_collapse_panel: 'Restrânge panoul',
ai_chat_thinking: 'Se gândește...',
ai_chat_working: 'Lucrează...',
ai_chat_exploring: 'Explorează...',
@@ -55,6 +55,7 @@ export const ru = {
annotation_button_label: 'Открыть аннотацию',
code_copied: 'Скопировано!',
code_copy: 'Копировать',
prompt_copy: 'Скопировать промпт',
code_block_collapsed: 'Показать все ${1} строк',
code_block_expanded: 'Показать меньше',
table_of_contents_button_label: 'Открыть оглавление',
@@ -75,7 +76,10 @@ export const ru = {
notfound_adaptive:
'Эта страница может существовать, но для доступа к ней может потребоваться вход.',
notfound_adaptive_login: 'Войти',
notfound_adaptive_registration_hint:
'Регистрация может быть доступна в зависимости от организации.',
notfound_goto_home: 'Перейти на главную',
notfound_suggestions_title: 'Возможно, вы ищете',
unexpected_error_title: 'Произошла ошибка',
unexpected_error: 'Извините, произошла непредвиденная ошибка. Пожалуйста, попробуйте позже.',
unexpected_error_retry: 'Попробуйте снова',
@@ -106,6 +110,8 @@ export const ru = {
ai_chat_assistant_greeting_evening: 'Добрый вечер',
ai_chat_assistant_greeting_night: 'Доброй ночи',
ai_chat_clear_conversation: 'Очистить диалог',
ai_chat_expand_panel: 'Развернуть панель',
ai_chat_collapse_panel: 'Свернуть панель',
ai_chat_thinking: 'Думает…',
ai_chat_working: 'Работает…',
ai_chat_exploring: 'Изучает…',
@@ -56,6 +56,7 @@ export const sk: TranslationLanguage = {
annotation_button_label: 'Otvoriť anotáciu',
code_copied: 'Skopírované!',
code_copy: 'Kopírovať',
prompt_copy: 'Kopírovať výzvu',
code_block_collapsed: 'Zobraziť všetkých ${1} riadkov',
code_block_expanded: 'Zobraziť menej',
table_of_contents_button_label: 'Otvoriť obsah',
@@ -76,7 +77,10 @@ export const sk: TranslationLanguage = {
notfound_adaptive:
'Táto stránka môže existovať, ale na prístup sa možno budete musieť prihlásiť.',
notfound_adaptive_login: 'Prihlásiť sa',
notfound_adaptive_registration_hint:
'Registrácia môže byť dostupná v závislosti od organizácie.',
notfound_goto_home: 'Prejsť na domovskú stránku',
notfound_suggestions_title: 'Možno hľadáte',
unexpected_error_title: 'Vyskytla sa chyba',
unexpected_error: 'Ľutujeme, vyskytla sa neočakávaná chyba. Skúste to znova neskôr.',
unexpected_error_retry: 'Skúsiť znova',
@@ -107,6 +111,8 @@ export const sk: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Dobrý večer',
ai_chat_assistant_greeting_night: 'Dobrú noc',
ai_chat_clear_conversation: 'Vymazať konverzáciu',
ai_chat_expand_panel: 'Rozbaliť panel',
ai_chat_collapse_panel: 'Zbaliť panel',
ai_chat_thinking: 'Premýšľam...',
ai_chat_working: 'Pracujem...',
ai_chat_exploring: 'Skúmam...',
@@ -56,6 +56,7 @@ export const sl: TranslationLanguage = {
annotation_button_label: 'Odpri opombo',
code_copied: 'Kopirano!',
code_copy: 'Kopiraj',
prompt_copy: 'Kopiraj poziv',
code_block_collapsed: 'Prikaži vseh ${1} vrstic',
code_block_expanded: 'Prikaži manj',
table_of_contents_button_label: 'Odpri kazalo',
@@ -75,7 +76,9 @@ export const sl: TranslationLanguage = {
notfound_adaptive_title: 'Stran ni na voljo',
notfound_adaptive: 'Ta stran morda obstaja, vendar se boste za dostop morda morali prijaviti.',
notfound_adaptive_login: 'Prijava',
notfound_adaptive_registration_hint: 'Registracija je morda na voljo, odvisno od organizacije.',
notfound_goto_home: 'Pojdi na domačo stran',
notfound_suggestions_title: 'Morda iščete',
unexpected_error_title: 'Prišlo je do napake',
unexpected_error: 'Žal je prišlo do nepričakovane napake. Poskusite znova pozneje.',
unexpected_error_retry: 'Poskusi znova',
@@ -106,6 +109,8 @@ export const sl: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Dober večer',
ai_chat_assistant_greeting_night: 'Lahko noč',
ai_chat_clear_conversation: 'Počisti pogovor',
ai_chat_expand_panel: 'Razširi ploščo',
ai_chat_collapse_panel: 'Skrči ploščo',
ai_chat_thinking: 'Razmišljam...',
ai_chat_working: 'Delam...',
ai_chat_exploring: 'Raziskujem...',
@@ -56,6 +56,7 @@ export const sv: TranslationLanguage = {
annotation_button_label: 'Öppna anteckning',
code_copied: 'Kopierat!',
code_copy: 'Kopiera',
prompt_copy: 'Kopiera prompten',
code_block_collapsed: 'Visa alla ${1} rader',
code_block_expanded: 'Visa mindre',
table_of_contents_button_label: 'Öppna innehållsförteckning',
@@ -75,7 +76,10 @@ export const sv: TranslationLanguage = {
notfound_adaptive_title: 'Sidan är inte tillgänglig',
notfound_adaptive: 'Sidan kan finnas, men du kan behöva logga in för att få åtkomst.',
notfound_adaptive_login: 'Logga in',
notfound_adaptive_registration_hint:
'Registrering kan vara tillgänglig beroende på organisationen.',
notfound_goto_home: 'Gå till startsidan',
notfound_suggestions_title: 'Du kanske letar efter',
unexpected_error_title: 'Ett fel uppstod',
unexpected_error: 'Tyvärr uppstod ett oväntat fel. Försök igen senare.',
unexpected_error_retry: 'Försök igen',
@@ -106,6 +110,8 @@ export const sv: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'God kväll',
ai_chat_assistant_greeting_night: 'God natt',
ai_chat_clear_conversation: 'Rensa konversation',
ai_chat_expand_panel: 'Expandera panel',
ai_chat_collapse_panel: 'Förminska panel',
ai_chat_thinking: 'Tänker...',
ai_chat_working: 'Arbetar...',
ai_chat_exploring: 'Utforskar...',
@@ -55,6 +55,7 @@ export const th: TranslationLanguage = {
annotation_button_label: 'เปิดคำอธิบายประกอบ',
code_copied: 'คัดลอกแล้ว!',
code_copy: 'คัดลอก',
prompt_copy: 'คัดลอกพรอมต์',
code_block_collapsed: 'แสดงทั้งหมด ${1} บรรทัด',
code_block_expanded: 'แสดงน้อยลง',
table_of_contents_button_label: 'เปิดสารบัญ',
@@ -74,7 +75,9 @@ export const th: TranslationLanguage = {
notfound_adaptive_title: 'หน้าไม่พร้อมใช้งาน',
notfound_adaptive: 'หน้านี้อาจมีอยู่ แต่คุณอาจต้องเข้าสู่ระบบเพื่อเข้าถึง',
notfound_adaptive_login: 'เข้าสู่ระบบ',
notfound_adaptive_registration_hint: 'การลงทะเบียนอาจใช้ได้ ขึ้นอยู่กับองค์กร',
notfound_goto_home: 'ไปที่หน้าแรก',
notfound_suggestions_title: 'คุณอาจกำลังมองหา',
unexpected_error_title: 'เกิดข้อผิดพลาด',
unexpected_error: 'ขออภัย เกิดข้อผิดพลาดที่ไม่คาดคิด โปรดลองอีกครั้งในภายหลัง',
unexpected_error_retry: 'ลองอีกครั้ง',
@@ -104,6 +107,8 @@ export const th: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'สวัสดีตอนเย็น',
ai_chat_assistant_greeting_night: 'ราตรีสวัสดิ์',
ai_chat_clear_conversation: 'ล้างการสนทนา',
ai_chat_expand_panel: 'ขยายแผง',
ai_chat_collapse_panel: 'ย่อแผง',
ai_chat_thinking: 'กำลังคิด...',
ai_chat_working: 'กำลังทำงาน...',
ai_chat_exploring: 'กำลังสำรวจ...',
@@ -56,6 +56,7 @@ export const tr: TranslationLanguage = {
annotation_button_label: 'Açıklamayı aç',
code_copied: 'Kopyalandı!',
code_copy: 'Kopyala',
prompt_copy: 'İstemi kopyala',
code_block_collapsed: 'Tüm ${1} satırı göster',
code_block_expanded: 'Daha az göster',
table_of_contents_button_label: 'İçindekiler tablosunu aç',
@@ -75,7 +76,9 @@ export const tr: TranslationLanguage = {
notfound_adaptive_title: 'Sayfa kullanılamıyor',
notfound_adaptive: 'Bu sayfa mevcut olabilir, ancak erişmek için oturum açmanız gerekebilir.',
notfound_adaptive_login: 'Oturum aç',
notfound_adaptive_registration_hint: 'Kayıt, kuruluşa bağlı olarak mevcut olabilir.',
notfound_goto_home: 'Ana sayfaya git',
notfound_suggestions_title: 'Şunu arıyor olabilirsiniz',
unexpected_error_title: 'Bir hata oluştu',
unexpected_error: 'Üzgünüz, beklenmeyen bir hata oluştu. Lütfen daha sonra tekrar deneyin.',
unexpected_error_retry: 'Tekrar dene',
@@ -105,6 +108,8 @@ export const tr: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'İyi akşamlar',
ai_chat_assistant_greeting_night: 'İyi geceler',
ai_chat_clear_conversation: 'Konuşmayı temizle',
ai_chat_expand_panel: 'Paneli genişlet',
ai_chat_collapse_panel: 'Paneli daralt',
ai_chat_thinking: 'Düşünüyor...',
ai_chat_working: 'Çalışıyor...',
ai_chat_exploring: 'İnceliyor...',
@@ -56,6 +56,7 @@ export const uk: TranslationLanguage = {
annotation_button_label: 'Відкрити примітку',
code_copied: 'Скопійовано!',
code_copy: 'Копіювати',
prompt_copy: 'Скопіювати промпт',
code_block_collapsed: 'Показати всі ${1} рядків',
code_block_expanded: 'Показати менше',
table_of_contents_button_label: 'Відкрити зміст',
@@ -75,7 +76,9 @@ export const uk: TranslationLanguage = {
notfound_adaptive_title: 'Сторінка недоступна',
notfound_adaptive: 'Ця сторінка може існувати, але для доступу до неї може знадобитися вхід.',
notfound_adaptive_login: 'Увійти',
notfound_adaptive_registration_hint: 'Реєстрація може бути доступною залежно від організації.',
notfound_goto_home: 'Перейти на головну',
notfound_suggestions_title: 'Можливо, ви шукаєте',
unexpected_error_title: 'Сталася помилка',
unexpected_error: 'Вибачте, сталася неочікувана помилка. Спробуйте ще раз пізніше.',
unexpected_error_retry: 'Спробувати ще раз',
@@ -105,6 +108,8 @@ export const uk: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Добрий вечір',
ai_chat_assistant_greeting_night: 'Добраніч',
ai_chat_clear_conversation: 'Очистити розмову',
ai_chat_expand_panel: 'Розгорнути панель',
ai_chat_collapse_panel: 'Згорнути панель',
ai_chat_thinking: 'Думаю...',
ai_chat_working: 'Працюю...',
ai_chat_exploring: 'Досліджую...',
@@ -56,6 +56,7 @@ export const vi: TranslationLanguage = {
annotation_button_label: 'Mở chú thích',
code_copied: 'Đã sao chép!',
code_copy: 'Sao chép',
prompt_copy: 'Sao chép lời nhắc',
code_block_collapsed: 'Hiển thị tất cả ${1} dòng',
code_block_expanded: 'Hiển thị ít hơn',
table_of_contents_button_label: 'Mở mục lục',
@@ -75,7 +76,9 @@ export const vi: TranslationLanguage = {
notfound_adaptive_title: 'Trang không khả dụng',
notfound_adaptive: 'Trang này có thể tồn tại, nhưng bạn có thể cần đăng nhập để truy cập.',
notfound_adaptive_login: 'Đăng nhập',
notfound_adaptive_registration_hint: 'Việc đăng ký có thể khả dụng tùy theo tổ chức.',
notfound_goto_home: 'Đi tới trang chủ',
notfound_suggestions_title: 'Có thể bạn đang tìm',
unexpected_error_title: 'Đã xảy ra lỗi',
unexpected_error: 'Rất tiếc, đã xảy ra lỗi không mong muốn. Vui lòng thử lại sau.',
unexpected_error_retry: 'Thử lại',
@@ -105,6 +108,8 @@ export const vi: TranslationLanguage = {
ai_chat_assistant_greeting_evening: 'Chào buổi tối',
ai_chat_assistant_greeting_night: 'Chúc ngủ ngon',
ai_chat_clear_conversation: 'Xóa cuộc trò chuyện',
ai_chat_expand_panel: 'Mở rộng bảng',
ai_chat_collapse_panel: 'Thu gọn bảng',
ai_chat_thinking: 'Đang suy nghĩ...',
ai_chat_working: 'Đang làm việc...',
ai_chat_exploring: 'Đang khám phá...',
@@ -55,6 +55,7 @@ export const yue: TranslationLanguage = {
annotation_button_label: '開啟註解',
code_copied: '已複製!',
code_copy: '複製',
prompt_copy: '複製提示',
code_block_collapsed: '顯示全部 ${1} 行',
code_block_expanded: '顯示少啲',
table_of_contents_button_label: '開啟目錄',
@@ -73,7 +74,9 @@ export const yue: TranslationLanguage = {
notfound_adaptive_title: '頁面無法使用',
notfound_adaptive: '呢個頁面可能存在,但你可能需要登入先可以存取。',
notfound_adaptive_login: '登入',
notfound_adaptive_registration_hint: '視乎機構,可能可以註冊。',
notfound_goto_home: '前往首頁',
notfound_suggestions_title: '你可能喺度搵緊',
unexpected_error_title: '發生錯誤',
unexpected_error: '抱歉,發生咗未預期嘅錯誤。請稍後再試。',
unexpected_error_retry: '重試',
@@ -103,6 +106,8 @@ export const yue: TranslationLanguage = {
ai_chat_assistant_greeting_evening: '晚上好',
ai_chat_assistant_greeting_night: '晚安',
ai_chat_clear_conversation: '清除對話',
ai_chat_expand_panel: '展開面板',
ai_chat_collapse_panel: '收合面板',
ai_chat_thinking: '諗緊...',
ai_chat_working: '處理緊...',
ai_chat_exploring: '探索緊...',
@@ -55,6 +55,7 @@ export const zh_tw: TranslationLanguage = {
annotation_button_label: '開啟註解',
code_copied: '已複製!',
code_copy: '複製',
prompt_copy: '複製提示詞',
code_block_collapsed: '顯示全部 ${1} 行',
code_block_expanded: '顯示較少',
table_of_contents_button_label: '開啟目錄',
@@ -73,7 +74,9 @@ export const zh_tw: TranslationLanguage = {
notfound_adaptive_title: '頁面無法使用',
notfound_adaptive: '此頁面可能存在,但您可能需要登入才能存取。',
notfound_adaptive_login: '登入',
notfound_adaptive_registration_hint: '視組織而定,可能可以註冊。',
notfound_goto_home: '前往首頁',
notfound_suggestions_title: '您可能在尋找',
unexpected_error_title: '發生錯誤',
unexpected_error: '抱歉,發生了未預期的錯誤。請稍後再試。',
unexpected_error_retry: '重試',
@@ -103,6 +106,8 @@ export const zh_tw: TranslationLanguage = {
ai_chat_assistant_greeting_evening: '晚安',
ai_chat_assistant_greeting_night: '晚安',
ai_chat_clear_conversation: '清除對話',
ai_chat_expand_panel: '展開面板',
ai_chat_collapse_panel: '收合面板',
ai_chat_thinking: '思考中...',
ai_chat_working: '處理中...',
ai_chat_exploring: '探索中...',
@@ -56,6 +56,7 @@ export const zh: TranslationLanguage = {
annotation_button_label: '打开批注',
code_copied: '已复制!',
code_copy: '复制',
prompt_copy: '复制提示词',
code_block_collapsed: '显示全部 ${1} 行',
code_block_expanded: '收起',
table_of_contents_button_label: '打开目录',
@@ -74,7 +75,9 @@ export const zh: TranslationLanguage = {
notfound_adaptive_title: '页面无法访问',
notfound_adaptive: '该页面可能存在,但您可能需要登录后才能访问。',
notfound_adaptive_login: '登录',
notfound_adaptive_registration_hint: '视组织而定,可能可以注册。',
notfound_goto_home: '前往首页',
notfound_suggestions_title: '您可能在寻找',
unexpected_error_title: '发生错误',
unexpected_error: '抱歉,发生了意外的错误。请稍后再试。',
unexpected_error_retry: '重试',
@@ -104,6 +107,8 @@ export const zh: TranslationLanguage = {
ai_chat_assistant_greeting_evening: '晚上好',
ai_chat_assistant_greeting_night: '晚安',
ai_chat_clear_conversation: '清空对话',
ai_chat_expand_panel: '展开面板',
ai_chat_collapse_panel: '收起面板',
ai_chat_thinking: '思考中...',
ai_chat_working: '工作中...',
ai_chat_exploring: '探索中...',
+14 -14
View File
@@ -222,7 +222,7 @@ function cacheLifeFromResponse(
}
const getUserById = cache(async (input: DataFetcherInput, params: { userId: string }) => {
'use cache: remote';
'use cache';
return wrapCacheDataFetcherError(async () => {
return trace(`getUserById(${params.userId})`, async () => {
const api = apiClient(input);
@@ -238,7 +238,7 @@ const getUserById = cache(async (input: DataFetcherInput, params: { userId: stri
const getSpace = cache(
async (input: DataFetcherInput, params: { spaceId: string; shareKey: string | undefined }) => {
'use cache: remote';
'use cache';
cacheTag(
getCacheTag({
tag: 'space',
@@ -268,7 +268,7 @@ const getSpace = cache(
const getChangeRequest = cache(
async (input: DataFetcherInput, params: { spaceId: string; changeRequestId: string }) => {
'use cache: remote';
'use cache';
cacheTag(
getCacheTag({
tag: 'change-request',
@@ -330,7 +330,7 @@ const getRevisionPageMarkdown = cache(
// used only to bust the cache
_functionName: string
) => {
'use cache: remote';
'use cache';
return wrapCacheDataFetcherError(async () => {
return trace(
`getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
@@ -372,7 +372,7 @@ const getRevisionPageDocument = cache(
// used only to bust the cache
_functionName: string
) => {
'use cache: remote';
'use cache';
return wrapCacheDataFetcherError(async () => {
return trace(
`getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
@@ -406,7 +406,7 @@ const getRevisionReusableContentDocument = cache(
input: DataFetcherInput,
params: { spaceId: string; revisionId: string; reusableContentId: string }
) => {
'use cache: remote';
'use cache';
return wrapCacheDataFetcherError(async () => {
return trace(
`getRevisionReusableContentDocument(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`,
@@ -439,7 +439,7 @@ const getRevisionPageByPath = cache(
input: DataFetcherInput,
params: { spaceId: string; revisionId: string; path: string }
) => {
'use cache: remote';
'use cache';
return wrapCacheDataFetcherError(async () => {
return trace(
`getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`,
@@ -466,7 +466,7 @@ const getRevisionPageByPath = cache(
const getDocument = cache(
async (input: DataFetcherInput, params: { spaceId: string; documentId: string }) => {
'use cache: remote';
'use cache';
return wrapCacheDataFetcherError(async () => {
return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => {
const api = apiClient(input);
@@ -496,7 +496,7 @@ const getComputedDocument = cache(
seed: string;
}
) => {
'use cache: remote';
'use cache';
cacheTag(
...getComputedContentSourceCacheTags(
{
@@ -571,7 +571,7 @@ const getPublishedContentSite = cache(
params: { organizationId: string; siteId: string; siteShareKey: string | undefined },
_apiVersion: string
) => {
'use cache: remote';
'use cache';
cacheTag(
getCacheTag({
tag: 'site',
@@ -613,7 +613,7 @@ const getSiteRedirectBySource = cache(
source: string;
}
) => {
'use cache: remote';
'use cache';
cacheTag(
getCacheTag({
tag: 'site',
@@ -648,7 +648,7 @@ const getSiteRedirectBySource = cache(
const getEmbedByUrl = cache(
async (input: DataFetcherInput, params: { spaceId: string; url: string }) => {
'use cache: remote';
'use cache';
cacheTag(
getCacheTag({
tag: 'space',
@@ -722,7 +722,7 @@ const renderIntegrationUi = cache(
input: DataFetcherInput,
params: { integrationName: string; request: RenderIntegrationUI }
) => {
'use cache: remote';
'use cache';
cacheTag(
getCacheTag({
tag: 'integration',
@@ -756,7 +756,7 @@ const listRevisionPageMetaLinks = cache(
input: DataFetcherInput,
params: { spaceId: string; revisionId: string; pageId: string }
) => {
'use cache: remote';
'use cache';
return wrapCacheDataFetcherError(async () => {
return trace(
`listRevisionPageMetaLinks(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
+9 -2
View File
@@ -816,11 +816,18 @@ function encodePathInSiteContent(
acceptsMarkdown(request);
if (pathname.match(MARKDOWN_PATH_REGEX) || shouldServeMarkdown) {
const pagePathWithoutMD = pathname.replace(MARKDOWN_PATH_REGEX, '');
const ask = new URL(request.url).searchParams.get('ask');
const searchParams = new URL(request.url).searchParams;
const ask = searchParams.get('ask');
// Optional end goal the calling agent is trying to accomplish, used to steer the answer.
// It is encoded as a second path segment (the route is statically rendered, so it can't
// read query params at runtime — the question is path-encoded for the same reason).
const goal = searchParams.get('goal');
return {
pathname:
typeof ask === 'string'
? `~gitbook/markdown-ask/${encodeURIComponent(ask)}`
? `~gitbook/markdown-ask/${encodeURIComponent(ask)}${
typeof goal === 'string' ? `/${encodeURIComponent(goal)}` : ''
}`
: `~gitbook/markdown/${encodePagePath(pagePathWithoutMD)}`,
routeType: 'static',
// TODO: track pageId / spaceId when possible
+24 -5
View File
@@ -8,12 +8,28 @@ import { filterOutNullable } from '@/lib/typescript';
import { serveMarkdown } from '@/routes/markdownPage';
import type { SearchAIAnswer, SearchAIAnswerSource } from '@gitbook/api';
/**
* Options to steer the AI answer served as markdown.
*/
export interface ServeAskMarkdownOptions {
/**
* The end goal the calling agent is trying to accomplish on behalf of the user,
* passed via the `?goal=` search parameter. Used by the backend to steer the answer.
*/
goal?: string;
}
/**
* Serve an AI answer as markdown for a page.
*/
export async function serveAskMarkdown(context: GitBookSiteContext, rawQuestion: string) {
export async function serveAskMarkdown(
context: GitBookSiteContext,
rawQuestion: string,
options: ServeAskMarkdownOptions = {}
) {
return serveMarkdown(async () => {
const question = rawQuestion.trim();
const goal = options.goal?.trim() || undefined;
if (
!question ||
@@ -32,6 +48,7 @@ export async function serveAskMarkdown(context: GitBookSiteContext, rawQuestion:
question,
context: {
siteSpaceId: context.siteSpace.id,
goal,
},
scope: {
mode: 'default',
@@ -80,10 +97,12 @@ export async function serveAskMarkdown(context: GitBookSiteContext, rawQuestion:
result +=
'If you need more information, consider asking one of these follow-up questions by performing an HTTP GET request on the URL:\n\n';
result += followupQuestions
.map(
(q) =>
`- [${q}](${context.linker.toAbsoluteURL(context.linker.toPathInSite(''))}?ask=${encodeURIComponent(q)})`
)
.map((q) => {
const base = `${context.linker.toAbsoluteURL(context.linker.toPathInSite(''))}?ask=${encodeURIComponent(q)}`;
// Carry the goal forward so a multi-step agent keeps its end goal across asks.
const url = goal ? `${base}&goal=${encodeURIComponent(goal)}` : base;
return `- [${q}](${url})`;
})
.join('\n');
result += '\n\n';
}
+17 -11
View File
@@ -57,17 +57,19 @@ If the exact page cannot be found, you can still retrieve the information using
### Option 1 Ask a question (recommended)
Perform an HTTP GET request on the documentation index with the \`ask\` parameter:
Perform an HTTP GET request on the documentation index with the \`ask\` parameter, and the optional \`goal\` parameter:
\`\`\`
GET ${context.linker.toAbsoluteURL(
context.linker.toPathForPagePath({
path: similarPages[0]?.path ?? 'docs/example',
})
)}?ask=<question>
)}?ask=<question>&goal=<end_goal>
\`\`\`
The question should be specific, self-contained, and written in natural language.
\`ask\` is the immediate question: it should be specific, self-contained, and written in natural language.
\`goal\` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.
### Option 2 Browse the documentation index
@@ -98,24 +100,28 @@ function renderAskFooter(
context: GitBookSiteContext,
pageLookup: ResolvedPagePath<RevisionPageDocument | RevisionPageGroup>
) {
const pageUrl = context.linker.toAbsoluteURL(
context.linker.toPathForPage({
page: pageLookup.page,
pages: context.revision.pages,
})
);
return `\n\n---\n\n# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.
## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.
Perform an HTTP GET request on the current page URL with the \`ask\` query parameter:
Perform an HTTP GET request on the current page URL with the \`ask\` query parameter, and the optional \`goal\` query parameter:
\`\`\`
GET ${context.linker.toAbsoluteURL(
context.linker.toPathForPage({
page: pageLookup.page,
pages: context.revision.pages,
})
)}?ask=<question>
GET ${pageUrl}?ask=<question>&goal=<endgoal>
\`\`\`
The question should be specific, self-contained, and written in natural language.
\`ask\` is the immediate question: it should be specific, self-contained, and written in natural language.
\`goal\` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.
Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
+7
View File
@@ -1,5 +1,12 @@
# @gitbook/react-openapi
## 1.5.14
### Patch Changes
- f4469b2: Support OpenAPI one-operation-per-page mode
- 88b94b5: Handle circular oneOf with discriminator and allOf in OpenAPI schemas
## 1.5.13
### Patch Changes
+1 -1
View File
@@ -7,7 +7,7 @@
"default": "./dist/index.js"
}
},
"version": "1.5.13",
"version": "1.5.14",
"sideEffects": false,
"dependencies": {
"@gitbook/expr": "workspace:*",