mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 01:53:26 +00:00
add navigateToPage assistant tool (#4299)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Add a `navigateToPage` assistant tool that lets the AI open a documentation page on the reader's behalf. The page opens instantly (no confirmation) and is pushed to the browser history so the reader can navigate back.
|
||||
@@ -0,0 +1,89 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { normalizePathname, resolveNavigationTarget, toInSiteHref } from './navigation';
|
||||
|
||||
const location = {
|
||||
href: 'https://docs.example.com/guides/intro',
|
||||
origin: 'https://docs.example.com',
|
||||
};
|
||||
|
||||
describe('resolveNavigationTarget', () => {
|
||||
it('resolves an absolute same-origin URL to a relative href and pathname', () => {
|
||||
expect(
|
||||
resolveNavigationTarget('https://docs.example.com/reference/models', location)
|
||||
).toEqual({ href: '/reference/models', pathname: '/reference/models' });
|
||||
});
|
||||
|
||||
it('keeps the query string and section anchor in href but not in pathname', () => {
|
||||
expect(
|
||||
resolveNavigationTarget(
|
||||
'https://docs.example.com/reference/models?tab=api#usage',
|
||||
location
|
||||
)
|
||||
).toEqual({ href: '/reference/models?tab=api#usage', pathname: '/reference/models' });
|
||||
});
|
||||
|
||||
it('resolves a relative path against the current location', () => {
|
||||
expect(resolveNavigationTarget('/reference/models', location)).toEqual({
|
||||
href: '/reference/models',
|
||||
pathname: '/reference/models',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a URL pointing to an external site', () => {
|
||||
const result = resolveNavigationTarget('https://evil.example.org/phishing', location);
|
||||
expect('error' in result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toInSiteHref', () => {
|
||||
// A docs site served under `/docs/` on a host shared with other content/sites.
|
||||
const linker = {
|
||||
siteBasePath: '/docs/',
|
||||
toLinkForContent: (url: string) => {
|
||||
const parsed = new URL(url);
|
||||
// Mirrors the real linker: in-site iff same host AND under the site base path.
|
||||
if (parsed.hostname === 'gitbook.com' && parsed.pathname.startsWith('/docs/')) {
|
||||
return parsed.pathname + parsed.search + parsed.hash;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
};
|
||||
|
||||
it('accepts an in-site absolute URL and returns a relative path', () => {
|
||||
expect(toInSiteHref('https://gitbook.com/docs/guides/intro', linker)).toBe(
|
||||
'/docs/guides/intro'
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts an in-site relative path under the site base path', () => {
|
||||
expect(toInSiteHref('/docs/guides/intro?x=1#y', linker)).toBe('/docs/guides/intro?x=1#y');
|
||||
});
|
||||
|
||||
it('rejects another page on the same host but outside the site base path', () => {
|
||||
// The reviewer's case: same host, different site/section.
|
||||
expect(toInSiteHref('https://gitbook.com/pricing', linker)).toBeNull();
|
||||
expect(toInSiteHref('/pricing', linker)).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects an external host', () => {
|
||||
expect(toInSiteHref('https://evil.example.org/docs/guides', linker)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizePathname', () => {
|
||||
it('strips a trailing slash', () => {
|
||||
expect(normalizePathname('/guides/intro/')).toBe('/guides/intro');
|
||||
});
|
||||
|
||||
it('keeps the root slash', () => {
|
||||
expect(normalizePathname('/')).toBe('/');
|
||||
});
|
||||
|
||||
it('decodes percent-encoding so encoded and decoded paths compare equal', () => {
|
||||
expect(normalizePathname('/h%C3%A9llo')).toBe(normalizePathname('/héllo'));
|
||||
});
|
||||
|
||||
it('treats encoded and decoded paths with a trailing slash as equal', () => {
|
||||
expect(normalizePathname('/caf%C3%A9/')).toBe(normalizePathname('/café'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { withTrailingSlash } from '@/lib/paths';
|
||||
import { isExternalLink } from '../utils/link';
|
||||
|
||||
/**
|
||||
* Resolve a link into a same-site target to navigate to.
|
||||
*
|
||||
* Returns an `error` when the URL is malformed or points outside of the documentation site,
|
||||
* so the assistant can be told it could not navigate.
|
||||
*/
|
||||
export function resolveNavigationTarget(
|
||||
url: string,
|
||||
location: { href: string; origin: string }
|
||||
): { href: string; pathname: string } | { error: string } {
|
||||
let target: URL;
|
||||
try {
|
||||
target = new URL(url, location.href);
|
||||
} catch {
|
||||
return { error: `Invalid URL: ${url}` };
|
||||
}
|
||||
|
||||
// Only allow navigating within the current documentation site to avoid sending the user to
|
||||
// an external website without their consent.
|
||||
if (isExternalLink(target.href, location.origin)) {
|
||||
return { error: 'Cannot navigate to a page outside of this documentation site.' };
|
||||
}
|
||||
|
||||
return { href: `${target.pathname}${target.search}${target.hash}`, pathname: target.pathname };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a site-relative href if `url` points within the current site, otherwise `null`.
|
||||
*
|
||||
* Unlike a bare same-origin check, this enforces the site base path, so an assistant cannot
|
||||
* navigate the reader to another page on the same host (e.g. a marketing page, or a different
|
||||
* docs site sharing the host such as `gitbook.com/other` or another `/url/...` proxied site).
|
||||
*/
|
||||
export function toInSiteHref(
|
||||
url: string,
|
||||
linker: { toLinkForContent: (url: string) => string; siteBasePath: string }
|
||||
): string | null {
|
||||
if (URL.canParse(url)) {
|
||||
// toLinkForContent returns a site-relative path for in-site URLs (matching host AND site
|
||||
// base path), or the raw absolute URL otherwise.
|
||||
const link = linker.toLinkForContent(url);
|
||||
return URL.canParse(link) ? null : link;
|
||||
}
|
||||
|
||||
// Relative path: it must live under the site base path.
|
||||
let pathname: string;
|
||||
let rest = '';
|
||||
try {
|
||||
const parsed = new URL(url, 'https://navigation.invalid');
|
||||
pathname = parsed.pathname;
|
||||
rest = `${parsed.search}${parsed.hash}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return withTrailingSlash(pathname).startsWith(linker.siteBasePath)
|
||||
? `${pathname}${rest}`
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a pathname so two equivalent paths compare equal regardless of percent-encoding or a
|
||||
* trailing slash (e.g. `/h%C3%A9llo/` and `/héllo`). Used to detect when an SPA navigation has
|
||||
* committed by comparing against `window.location.pathname`.
|
||||
*/
|
||||
export function normalizePathname(pathname: string): string {
|
||||
let decoded = pathname;
|
||||
try {
|
||||
decoded = decodeURIComponent(pathname);
|
||||
} catch {
|
||||
// Keep the raw value if it isn't valid percent-encoding.
|
||||
}
|
||||
return decoded.length > 1 && decoded.endsWith('/') ? decoded.slice(0, -1) : decoded;
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './types';
|
||||
export * from './chat';
|
||||
export * from './navigate';
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
'use server';
|
||||
|
||||
import { resolveContentRef, resolveStringContentRef } from '@/lib/references';
|
||||
import { fetchServerActionSiteContext, getServerActionBaseContext } from '@/lib/server-actions';
|
||||
import { traceErrorOnly } from '@/lib/tracing';
|
||||
import { toInSiteHref } from '../navigation';
|
||||
|
||||
/**
|
||||
* Resolve a link provided by the assistant into a path that can be navigated to within the site.
|
||||
*
|
||||
* The assistant references pages using the stable content-ref scheme (e.g.
|
||||
* `/spaces/<spaceId>/pages/<pageId>`). Those URLs are not directly navigable in the published
|
||||
* site, so we resolve them to the real site link using the site context. Any other URL is only
|
||||
* accepted if it points within the current site, so the assistant cannot navigate the reader off
|
||||
* the documentation site.
|
||||
*/
|
||||
export async function resolveAINavigationLink(
|
||||
url: string
|
||||
): Promise<{ href: string } | { error: string }> {
|
||||
return traceErrorOnly('AI.resolveAINavigationLink', async () => {
|
||||
const baseContext = await getServerActionBaseContext();
|
||||
const context = await fetchServerActionSiteContext(baseContext);
|
||||
|
||||
// The content-ref scheme operates on the path portion of the URL. Strip any origin so an
|
||||
// absolute URL (e.g. `https://docs.example.com/spaces/.../pages/...`) is handled too.
|
||||
let path = url;
|
||||
if (URL.canParse(url)) {
|
||||
const parsed = new URL(url);
|
||||
path = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
||||
}
|
||||
|
||||
const contentRef = resolveStringContentRef(path);
|
||||
if (contentRef) {
|
||||
const resolved = await resolveContentRef(contentRef, context);
|
||||
if (!resolved) {
|
||||
return { error: `Could not resolve page for ${url}` };
|
||||
}
|
||||
return { href: resolved.href };
|
||||
}
|
||||
|
||||
// Not a content reference: only navigate to it if it points within the current site.
|
||||
const inSiteHref = toInSiteHref(url, context.linker);
|
||||
if (!inSiteHref) {
|
||||
return { error: 'Cannot navigate to a page outside of this documentation site.' };
|
||||
}
|
||||
return { href: inSiteHref };
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,9 @@ import type { GitBookIntegrationTool } from '@gitbook/browser-types';
|
||||
import { integrationsAssistantTools } from '../Integrations';
|
||||
import { type AnyAIControlTool, getControlTools } from './controls';
|
||||
|
||||
export function getTools(): (GitBookIntegrationTool | AnyAIControlTool)[] {
|
||||
export function getTools(
|
||||
builtInTools: GitBookIntegrationTool[] = []
|
||||
): (GitBookIntegrationTool | AnyAIControlTool)[] {
|
||||
const integrationTools = integrationsAssistantTools.getState().tools;
|
||||
return [...getControlTools(), ...integrationTools];
|
||||
return [...getControlTools(), ...builtInTools, ...integrationTools];
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { type AIChatReference, serializeReferences } from './references';
|
||||
import { type RenderAIMessageOptions, streamAIChatResponse } from './server-actions';
|
||||
import { getTools } from './tools';
|
||||
import { useAIMessageContextRef } from './useAIMessageContext';
|
||||
import { useNavigateToPageTool } from './useNavigateToPageTool';
|
||||
|
||||
export type AIChatMessage = {
|
||||
role: AIMessageRole;
|
||||
@@ -192,6 +193,10 @@ export function AIChatProvider(props: {
|
||||
const { siteSpaceId } = useCurrentContent();
|
||||
const language = useLanguage();
|
||||
|
||||
// Built-in tools exposed to the assistant (e.g. navigating to a page). The tool has a stable
|
||||
// identity, so it can be referenced directly from the streaming callback.
|
||||
const navigateToPageTool = useNavigateToPageTool();
|
||||
|
||||
// Event listeners storage
|
||||
const eventsRef = React.useRef<Map<AIChatEvent['type'], AIChatEventListener[]>>(new Map());
|
||||
|
||||
@@ -256,7 +261,7 @@ export function AIChatProvider(props: {
|
||||
|
||||
// Execute a tool call
|
||||
const executeToolCall = async (event: AIStreamResponseToolCallPending) => {
|
||||
const tools = getTools();
|
||||
const tools = getTools([navigateToPageTool]);
|
||||
const toolDef = tools.find((tool) => tool.name === event.toolCall.tool);
|
||||
|
||||
if (!toolDef || !('execute' in toolDef)) {
|
||||
@@ -292,7 +297,7 @@ export function AIChatProvider(props: {
|
||||
|
||||
let toolToExecute: AIStreamResponseToolCallPending | null = null;
|
||||
try {
|
||||
const tools = getTools();
|
||||
const tools = getTools([navigateToPageTool]);
|
||||
const stream = await streamAIChatResponse({
|
||||
message: input.message,
|
||||
toolCall: input.toolCall,
|
||||
@@ -473,6 +478,7 @@ export function AIChatProvider(props: {
|
||||
renderMessageOptions?.withToolCalls,
|
||||
renderMessageOptions?.asEmbeddable,
|
||||
language,
|
||||
navigateToPageTool,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
'use client';
|
||||
|
||||
import { useLanguage } from '@/intl/client';
|
||||
import { tString } from '@/intl/translate';
|
||||
import type { AIToolDefinition } from '@gitbook/api';
|
||||
import type { GitBookIntegrationTool } from '@gitbook/browser-types';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
import { NavigationStatusContext } from '../hooks';
|
||||
import { normalizePathname, resolveNavigationTarget } from './navigation';
|
||||
import { resolveAINavigationLink } from './server-actions';
|
||||
|
||||
const NavigateToPageInputSchema = z.object({
|
||||
url: z
|
||||
.string()
|
||||
.describe(
|
||||
'The URL of the documentation page to open. Must be a page within this documentation site (the same URL you would use to link to the page). Can include a section anchor (e.g. #section).'
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
* Resolve once the SPA navigation to `pathname` has committed (the browser URL reflects it), or
|
||||
* after a timeout. App Router updates `window.location` only when the navigation commits, so this
|
||||
* lets the tool hold its turn until the user is actually on the new page — after which the
|
||||
* tool-result server action's router refresh can no longer cancel the navigation.
|
||||
*/
|
||||
function waitForNavigationCommit(pathname: string): Promise<boolean> {
|
||||
const target = normalizePathname(pathname);
|
||||
if (normalizePathname(window.location.pathname) === target) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const startedAt = Date.now();
|
||||
const check = () => {
|
||||
if (normalizePathname(window.location.pathname) === target) {
|
||||
resolve(true);
|
||||
} else if (Date.now() - startedAt > 3000) {
|
||||
resolve(false);
|
||||
} else {
|
||||
requestAnimationFrame(check);
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(check);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the built-in `navigateToPage` tool exposed to the assistant.
|
||||
*
|
||||
* The tool opens a page within the current documentation site without confirmation. It navigates
|
||||
* instantly with the Next.js router (adding a browser history entry, so the user can navigate
|
||||
* back) and waits for the navigation to commit before reporting back, so the assistant's
|
||||
* follow-up does not cancel the navigation.
|
||||
*/
|
||||
export function useNavigateToPageTool(): GitBookIntegrationTool {
|
||||
const router = useRouter();
|
||||
const language = useLanguage();
|
||||
const { onNavigationClick } = React.useContext(NavigationStatusContext);
|
||||
|
||||
// The tool object is memoized once, so read the latest values from a ref at call time.
|
||||
const ref = React.useRef({ router, language, onNavigationClick });
|
||||
React.useEffect(() => {
|
||||
ref.current = { router, language, onNavigationClick };
|
||||
});
|
||||
|
||||
return React.useMemo<GitBookIntegrationTool>(
|
||||
() => ({
|
||||
name: 'navigateToPage',
|
||||
description:
|
||||
'Navigate the user to a page in the documentation. The page opens instantly without asking for confirmation, so only use it when the user clearly wants to be taken to a specific page. Provide the URL of the page within this documentation site.',
|
||||
inputSchema: zodToJsonSchema(
|
||||
NavigateToPageInputSchema as any
|
||||
) as AIToolDefinition['inputSchema'],
|
||||
execute: async (input) => {
|
||||
const { router, language, onNavigationClick } = ref.current;
|
||||
const { url } = NavigateToPageInputSchema.parse(input);
|
||||
|
||||
// The assistant references pages using the stable content-ref scheme
|
||||
// (e.g. `/spaces/<id>/pages/<id>`). Resolve it server-side to the real site link.
|
||||
const resolved = await resolveAINavigationLink(url);
|
||||
const target =
|
||||
'error' in resolved
|
||||
? resolved
|
||||
: resolveNavigationTarget(resolved.href, window.location);
|
||||
|
||||
if ('error' in target) {
|
||||
return {
|
||||
output: { error: target.error },
|
||||
summary: {
|
||||
icon: 'triangle-exclamation',
|
||||
text: tString(language, 'ai_chat_tools_navigate_failed'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
onNavigationClick(target.href);
|
||||
router.push(target.href);
|
||||
const committed = await waitForNavigationCommit(target.pathname);
|
||||
if (!committed) {
|
||||
// biome-ignore lint/suspicious/noConsole: surfaces a navigation that never committed
|
||||
console.warn(`navigateToPage: navigation to ${target.href} did not commit`);
|
||||
}
|
||||
|
||||
return {
|
||||
output: { navigated: true, url: target.href },
|
||||
summary: {
|
||||
icon: 'book-open',
|
||||
text: tString(language, 'ai_chat_tools_navigated_to_page'),
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
}
|
||||
@@ -135,6 +135,8 @@ export const ar: TranslationLanguage = {
|
||||
searched_for: 'تم البحث عن ${1}',
|
||||
ai_chat_tools_listed_pages: 'تم تصفح الوثائق',
|
||||
ai_chat_tools_read_page: 'تمت قراءة الصفحة ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'تم فتح الصفحة',
|
||||
ai_chat_tools_navigate_failed: 'تعذّر فتح الصفحة',
|
||||
ai_chat_tools_mcp_tool: 'تم استدعاء ${1}',
|
||||
ai_chat_ask: 'اسأل ${1}',
|
||||
ai_chat_ask_about_page: 'اسأل ${1} عن هذه الصفحة',
|
||||
|
||||
@@ -137,6 +137,8 @@ export const bg: TranslationLanguage = {
|
||||
searched_for: 'Търсено ${1}',
|
||||
ai_chat_tools_listed_pages: 'Прегледа документацията',
|
||||
ai_chat_tools_read_page: 'Прочете страница ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Страницата е отворена',
|
||||
ai_chat_tools_navigate_failed: 'Страницата не може да бъде отворена',
|
||||
ai_chat_tools_mcp_tool: 'Извика ${1}',
|
||||
ai_chat_ask: 'Попитайте ${1}',
|
||||
ai_chat_ask_about_page: 'Попитайте ${1} за тази страница',
|
||||
|
||||
@@ -137,6 +137,8 @@ export const cs: TranslationLanguage = {
|
||||
searched_for: 'Hledáno ${1}',
|
||||
ai_chat_tools_listed_pages: 'Procházel dokumentaci',
|
||||
ai_chat_tools_read_page: 'Přečetl stránku ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Stránka otevřena',
|
||||
ai_chat_tools_navigate_failed: 'Stránku se nepodařilo otevřít',
|
||||
ai_chat_tools_mcp_tool: 'Zavolal ${1}',
|
||||
ai_chat_ask: 'Zeptat se ${1}',
|
||||
ai_chat_ask_about_page: 'Zeptat se ${1} na tuto stránku',
|
||||
|
||||
@@ -136,6 +136,8 @@ export const da: TranslationLanguage = {
|
||||
searched_for: 'Søgte efter ${1}',
|
||||
ai_chat_tools_listed_pages: 'Gennemså dokumentationen',
|
||||
ai_chat_tools_read_page: 'Læste side ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Åbnede siden',
|
||||
ai_chat_tools_navigate_failed: 'Kunne ikke åbne siden',
|
||||
ai_chat_tools_mcp_tool: 'Kaldte ${1}',
|
||||
ai_chat_ask: 'Spørg ${1}',
|
||||
ai_chat_ask_about_page: 'Spørg ${1} om denne side',
|
||||
|
||||
@@ -139,6 +139,8 @@ export const de = {
|
||||
searched_for: 'Gesucht nach ${1}',
|
||||
ai_chat_tools_listed_pages: 'Docs durchsucht',
|
||||
ai_chat_tools_read_page: 'Seite ${1} gelesen',
|
||||
ai_chat_tools_navigated_to_page: 'Seite geöffnet',
|
||||
ai_chat_tools_navigate_failed: 'Seite konnte nicht geöffnet werden',
|
||||
ai_chat_tools_mcp_tool: '${1} aufgerufen',
|
||||
ai_chat_ask: '${1} fragen',
|
||||
ai_chat_ask_about_page: '${1} zu dieser Seite befragen',
|
||||
|
||||
@@ -139,6 +139,8 @@ export const el: TranslationLanguage = {
|
||||
searched_for: 'Αναζήτηση για ${1}',
|
||||
ai_chat_tools_listed_pages: 'Περιηγήθηκε στην τεκμηρίωση',
|
||||
ai_chat_tools_read_page: 'Διαβάστηκε η σελίδα ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Άνοιξε η σελίδα',
|
||||
ai_chat_tools_navigate_failed: 'Αποτυχία ανοίγματος της σελίδας',
|
||||
ai_chat_tools_mcp_tool: 'Κλήθηκε ${1}',
|
||||
ai_chat_ask: 'Ρωτήστε ${1}',
|
||||
ai_chat_ask_about_page: 'Ρωτήστε ${1} για αυτήν τη σελίδα',
|
||||
|
||||
@@ -133,6 +133,8 @@ export const en = {
|
||||
searched_for: 'Searched for ${1}',
|
||||
ai_chat_tools_listed_pages: 'Browsed the docs',
|
||||
ai_chat_tools_read_page: 'Read page ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Opened the page',
|
||||
ai_chat_tools_navigate_failed: 'Failed to open the page',
|
||||
ai_chat_tools_mcp_tool: 'Called ${1}',
|
||||
ai_chat_ask: 'Ask ${1}',
|
||||
ai_chat_ask_about_page: 'Ask ${1} about this page',
|
||||
|
||||
@@ -139,6 +139,8 @@ export const es: TranslationLanguage = {
|
||||
searched_for: 'Se buscó: ${1}',
|
||||
ai_chat_tools_listed_pages: 'Exploró los docs',
|
||||
ai_chat_tools_read_page: 'Leyó la página ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Página abierta',
|
||||
ai_chat_tools_navigate_failed: 'No se pudo abrir la página',
|
||||
ai_chat_tools_mcp_tool: 'Llamó a ${1}',
|
||||
ai_chat_ask: 'Preguntar a ${1}',
|
||||
ai_chat_ask_about_page: 'Preguntar a ${1} sobre esta página',
|
||||
|
||||
@@ -135,6 +135,8 @@ export const et: TranslationLanguage = {
|
||||
searched_for: 'Otsiti ${1}',
|
||||
ai_chat_tools_listed_pages: 'Sirvis dokumentatsiooni',
|
||||
ai_chat_tools_read_page: 'Luges lehte ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Leht avatud',
|
||||
ai_chat_tools_navigate_failed: 'Lehe avamine ebaõnnestus',
|
||||
ai_chat_tools_mcp_tool: 'Kutsus ${1}',
|
||||
ai_chat_ask: 'Küsi ${1}',
|
||||
ai_chat_ask_about_page: 'Küsi ${1} selle lehe kohta',
|
||||
|
||||
@@ -136,6 +136,8 @@ export const fi: TranslationLanguage = {
|
||||
searched_for: 'Haettu: ${1}',
|
||||
ai_chat_tools_listed_pages: 'Selattiin dokumentaatiota',
|
||||
ai_chat_tools_read_page: 'Luettiin sivu ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Sivu avattu',
|
||||
ai_chat_tools_navigate_failed: 'Sivun avaaminen epäonnistui',
|
||||
ai_chat_tools_mcp_tool: 'Kutsuttiin ${1}',
|
||||
ai_chat_ask: 'Kysy ${1}',
|
||||
ai_chat_ask_about_page: 'Kysy ${1} tältä sivulta',
|
||||
|
||||
@@ -134,6 +134,8 @@ export const fr = {
|
||||
searched_for: 'Recherche : ${1}',
|
||||
ai_chat_tools_listed_pages: 'A parcouru la documentation',
|
||||
ai_chat_tools_read_page: 'A consulté la page ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Page ouverte',
|
||||
ai_chat_tools_navigate_failed: "Échec de l'ouverture de la page",
|
||||
ai_chat_tools_mcp_tool: 'A appelé ${1}',
|
||||
ai_chat_ask: 'Demander à ${1}',
|
||||
ai_chat_ask_about_page: 'Demander à ${1} à propos de cette page',
|
||||
|
||||
@@ -134,6 +134,8 @@ export const he: TranslationLanguage = {
|
||||
searched_for: 'חיפש ${1}',
|
||||
ai_chat_tools_listed_pages: 'עיין בתיעוד',
|
||||
ai_chat_tools_read_page: 'קרא את הדף ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'הדף נפתח',
|
||||
ai_chat_tools_navigate_failed: 'פתיחת הדף נכשלה',
|
||||
ai_chat_tools_mcp_tool: 'קרא ל-${1}',
|
||||
ai_chat_ask: 'שאל את ${1}',
|
||||
ai_chat_ask_about_page: 'שאל את ${1} על הדף הזה',
|
||||
|
||||
@@ -135,6 +135,8 @@ export const hi: TranslationLanguage = {
|
||||
searched_for: '${1} के लिए खोजा गया',
|
||||
ai_chat_tools_listed_pages: 'दस्तावेज़ ब्राउज़ किए',
|
||||
ai_chat_tools_read_page: 'पृष्ठ ${1} पढ़ा',
|
||||
ai_chat_tools_navigated_to_page: 'पेज खोला गया',
|
||||
ai_chat_tools_navigate_failed: 'पेज खोलने में विफल',
|
||||
ai_chat_tools_mcp_tool: '${1} को कॉल किया',
|
||||
ai_chat_ask: '${1} से पूछें',
|
||||
ai_chat_ask_about_page: 'इस पृष्ठ के बारे में ${1} से पूछें',
|
||||
|
||||
@@ -137,6 +137,8 @@ export const hr: TranslationLanguage = {
|
||||
searched_for: 'Pretraženo ${1}',
|
||||
ai_chat_tools_listed_pages: 'Pregledao dokumentaciju',
|
||||
ai_chat_tools_read_page: 'Pročitao stranicu ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Stranica otvorena',
|
||||
ai_chat_tools_navigate_failed: 'Stranicu nije moguće otvoriti',
|
||||
ai_chat_tools_mcp_tool: 'Pozvao ${1}',
|
||||
ai_chat_ask: 'Pitaj ${1}',
|
||||
ai_chat_ask_about_page: 'Pitaj ${1} o ovoj stranici',
|
||||
|
||||
@@ -137,6 +137,8 @@ export const hu: TranslationLanguage = {
|
||||
searched_for: 'Keresve: ${1}',
|
||||
ai_chat_tools_listed_pages: 'Dokumentáció böngészve',
|
||||
ai_chat_tools_read_page: '${1} oldal elolvasva',
|
||||
ai_chat_tools_navigated_to_page: 'Oldal megnyitva',
|
||||
ai_chat_tools_navigate_failed: 'Az oldal megnyitása sikertelen',
|
||||
ai_chat_tools_mcp_tool: '${1} meghívva',
|
||||
ai_chat_ask: '${1} kérdezése',
|
||||
ai_chat_ask_about_page: '${1} kérdezése erről az oldalról',
|
||||
|
||||
@@ -136,6 +136,8 @@ export const id: TranslationLanguage = {
|
||||
searched_for: 'Mencari ${1}',
|
||||
ai_chat_tools_listed_pages: 'Menelusuri dokumentasi',
|
||||
ai_chat_tools_read_page: 'Membaca halaman ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Halaman dibuka',
|
||||
ai_chat_tools_navigate_failed: 'Gagal membuka halaman',
|
||||
ai_chat_tools_mcp_tool: 'Memanggil ${1}',
|
||||
ai_chat_ask: 'Tanya ${1}',
|
||||
ai_chat_ask_about_page: 'Tanya ${1} tentang halaman ini',
|
||||
|
||||
@@ -138,6 +138,8 @@ export const it: TranslationLanguage = {
|
||||
searched_for: 'Ricerca: ${1}',
|
||||
ai_chat_tools_listed_pages: 'Ha esplorato la documentazione',
|
||||
ai_chat_tools_read_page: 'Ha letto la pagina ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Pagina aperta',
|
||||
ai_chat_tools_navigate_failed: 'Impossibile aprire la pagina',
|
||||
ai_chat_tools_mcp_tool: 'Ha chiamato ${1}',
|
||||
ai_chat_ask: 'Chiedi a ${1}',
|
||||
ai_chat_ask_about_page: 'Chiedi a ${1} riguardo a questa pagina',
|
||||
|
||||
@@ -137,6 +137,8 @@ export const ja: TranslationLanguage = {
|
||||
searched_for: '${1}を検索しました',
|
||||
ai_chat_tools_listed_pages: 'ドキュメントを閲覧',
|
||||
ai_chat_tools_read_page: 'ページ ${1} を読みました',
|
||||
ai_chat_tools_navigated_to_page: 'ページを開きました',
|
||||
ai_chat_tools_navigate_failed: 'ページを開けませんでした',
|
||||
ai_chat_tools_mcp_tool: '${1} を呼び出しました',
|
||||
ai_chat_ask: '${1} に質問する',
|
||||
ai_chat_ask_about_page: 'このページについて ${1} に質問する',
|
||||
|
||||
@@ -136,6 +136,8 @@ export const ko: TranslationLanguage = {
|
||||
searched_for: '${1} 검색함',
|
||||
ai_chat_tools_listed_pages: '문서를 탐색함',
|
||||
ai_chat_tools_read_page: '${1} 페이지를 읽음',
|
||||
ai_chat_tools_navigated_to_page: '페이지를 열었습니다',
|
||||
ai_chat_tools_navigate_failed: '페이지를 열지 못했습니다',
|
||||
ai_chat_tools_mcp_tool: '${1} 호출함',
|
||||
ai_chat_ask: '${1}에게 질문',
|
||||
ai_chat_ask_about_page: '이 페이지에 대해 ${1}에게 질문',
|
||||
|
||||
@@ -136,6 +136,8 @@ export const lt: TranslationLanguage = {
|
||||
searched_for: 'Ieškota ${1}',
|
||||
ai_chat_tools_listed_pages: 'Naršyta dokumentacija',
|
||||
ai_chat_tools_read_page: 'Perskaitytas puslapis ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Puslapis atidarytas',
|
||||
ai_chat_tools_navigate_failed: 'Nepavyko atidaryti puslapio',
|
||||
ai_chat_tools_mcp_tool: 'Iškviesta ${1}',
|
||||
ai_chat_ask: 'Klausti ${1}',
|
||||
ai_chat_ask_about_page: 'Klausti ${1} apie šį puslapį',
|
||||
|
||||
@@ -135,6 +135,8 @@ export const lv: TranslationLanguage = {
|
||||
searched_for: 'Meklēts ${1}',
|
||||
ai_chat_tools_listed_pages: 'Pārlūkoja dokumentāciju',
|
||||
ai_chat_tools_read_page: 'Izlasīja lapu ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Lapa atvērta',
|
||||
ai_chat_tools_navigate_failed: 'Neizdevās atvērt lapu',
|
||||
ai_chat_tools_mcp_tool: 'Izsauca ${1}',
|
||||
ai_chat_ask: 'Jautāt ${1}',
|
||||
ai_chat_ask_about_page: 'Jautāt ${1} par šo lapu',
|
||||
|
||||
@@ -136,6 +136,8 @@ export const ms: TranslationLanguage = {
|
||||
searched_for: 'Mencari ${1}',
|
||||
ai_chat_tools_listed_pages: 'Melayari dokumentasi',
|
||||
ai_chat_tools_read_page: 'Membaca halaman ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Halaman dibuka',
|
||||
ai_chat_tools_navigate_failed: 'Gagal membuka halaman',
|
||||
ai_chat_tools_mcp_tool: 'Memanggil ${1}',
|
||||
ai_chat_ask: 'Tanya ${1}',
|
||||
ai_chat_ask_about_page: 'Tanya ${1} tentang halaman ini',
|
||||
|
||||
@@ -137,6 +137,8 @@ export const nl: TranslationLanguage = {
|
||||
searched_for: 'Gezocht naar ${1}',
|
||||
ai_chat_tools_listed_pages: 'Docs doorzocht',
|
||||
ai_chat_tools_read_page: 'Pagina ${1} gelezen',
|
||||
ai_chat_tools_navigated_to_page: 'Pagina geopend',
|
||||
ai_chat_tools_navigate_failed: 'Kan de pagina niet openen',
|
||||
ai_chat_tools_mcp_tool: '${1} aangeroepen',
|
||||
ai_chat_ask: 'Vraag het aan ${1}',
|
||||
ai_chat_ask_about_page: 'Vraag ${1} naar deze pagina',
|
||||
|
||||
@@ -137,6 +137,8 @@ export const no: TranslationLanguage = {
|
||||
searched_for: 'Søkte etter ${1}',
|
||||
ai_chat_tools_listed_pages: 'Bladde gjennom docs',
|
||||
ai_chat_tools_read_page: 'Leste side ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Åpnet siden',
|
||||
ai_chat_tools_navigate_failed: 'Kunne ikke åpne siden',
|
||||
ai_chat_tools_mcp_tool: 'Kalte ${1}',
|
||||
ai_chat_ask: 'Spør ${1}',
|
||||
ai_chat_ask_about_page: 'Spør ${1} om denne siden',
|
||||
|
||||
@@ -136,6 +136,8 @@ export const pl: TranslationLanguage = {
|
||||
searched_for: 'Wyszukano ${1}',
|
||||
ai_chat_tools_listed_pages: 'Przeglądano dokumentację',
|
||||
ai_chat_tools_read_page: 'Przeczytano stronę ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Otwarto stronę',
|
||||
ai_chat_tools_navigate_failed: 'Nie udało się otworzyć strony',
|
||||
ai_chat_tools_mcp_tool: 'Wywołano ${1}',
|
||||
ai_chat_ask: 'Zapytaj ${1}',
|
||||
ai_chat_ask_about_page: 'Zapytaj ${1} o tę stronę',
|
||||
|
||||
@@ -137,6 +137,8 @@ export const pt_br = {
|
||||
searched_for: 'Pesquisou por ${1}',
|
||||
ai_chat_tools_listed_pages: 'Navegou pelos docs',
|
||||
ai_chat_tools_read_page: 'Leu a página ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Página aberta',
|
||||
ai_chat_tools_navigate_failed: 'Falha ao abrir a página',
|
||||
ai_chat_tools_mcp_tool: 'Chamou ${1}',
|
||||
ai_chat_ask: 'Perguntar a ${1}',
|
||||
ai_chat_ask_about_page: 'Perguntar a ${1} sobre esta página',
|
||||
|
||||
@@ -136,6 +136,8 @@ export const pt: TranslationLanguage = {
|
||||
searched_for: 'Pesquisado por ${1}',
|
||||
ai_chat_tools_listed_pages: 'Consultou a documentação',
|
||||
ai_chat_tools_read_page: 'Leu a página ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Página aberta',
|
||||
ai_chat_tools_navigate_failed: 'Falha ao abrir a página',
|
||||
ai_chat_tools_mcp_tool: 'Chamou ${1}',
|
||||
ai_chat_ask: 'Perguntar a ${1}',
|
||||
ai_chat_ask_about_page: 'Perguntar a ${1} sobre esta página',
|
||||
|
||||
@@ -138,6 +138,8 @@ export const ro: TranslationLanguage = {
|
||||
searched_for: 'S-a căutat ${1}',
|
||||
ai_chat_tools_listed_pages: 'A răsfoit documentația',
|
||||
ai_chat_tools_read_page: 'A citit pagina ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Pagina a fost deschisă',
|
||||
ai_chat_tools_navigate_failed: 'Deschiderea paginii a eșuat',
|
||||
ai_chat_tools_mcp_tool: 'A apelat ${1}',
|
||||
ai_chat_ask: 'Întreabă ${1}',
|
||||
ai_chat_ask_about_page: 'Întreabă ${1} despre această pagină',
|
||||
|
||||
@@ -136,6 +136,8 @@ export const ru = {
|
||||
searched_for: 'Выполнен поиск ${1}',
|
||||
ai_chat_tools_listed_pages: 'Просмотрены документы',
|
||||
ai_chat_tools_read_page: 'Прочитана страница ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Страница открыта',
|
||||
ai_chat_tools_navigate_failed: 'Не удалось открыть страницу',
|
||||
ai_chat_tools_mcp_tool: 'Вызван ${1}',
|
||||
ai_chat_ask: 'Спросить у ${1}',
|
||||
ai_chat_ask_about_page: 'Спросить у ${1} об этой странице',
|
||||
|
||||
@@ -137,6 +137,8 @@ export const sk: TranslationLanguage = {
|
||||
searched_for: 'Hľadané ${1}',
|
||||
ai_chat_tools_listed_pages: 'Prehliadal dokumentáciu',
|
||||
ai_chat_tools_read_page: 'Prečítal stránku ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Stránka otvorená',
|
||||
ai_chat_tools_navigate_failed: 'Stránku sa nepodarilo otvoriť',
|
||||
ai_chat_tools_mcp_tool: 'Zavolal ${1}',
|
||||
ai_chat_ask: 'Opýtať sa ${1}',
|
||||
ai_chat_ask_about_page: 'Opýtať sa ${1} na túto stránku',
|
||||
|
||||
@@ -136,6 +136,8 @@ export const sl: TranslationLanguage = {
|
||||
searched_for: 'Iskano ${1}',
|
||||
ai_chat_tools_listed_pages: 'Prebrskana dokumentacija',
|
||||
ai_chat_tools_read_page: 'Prebrana stran ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Stran odprta',
|
||||
ai_chat_tools_navigate_failed: 'Strani ni bilo mogoče odpreti',
|
||||
ai_chat_tools_mcp_tool: 'Poklicano ${1}',
|
||||
ai_chat_ask: 'Vprašaj ${1}',
|
||||
ai_chat_ask_about_page: 'Vprašaj ${1} o tej strani',
|
||||
|
||||
@@ -136,6 +136,8 @@ export const sv: TranslationLanguage = {
|
||||
searched_for: 'Sökte efter ${1}',
|
||||
ai_chat_tools_listed_pages: 'Bläddrade i dokumentationen',
|
||||
ai_chat_tools_read_page: 'Läste sidan ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Öppnade sidan',
|
||||
ai_chat_tools_navigate_failed: 'Det gick inte att öppna sidan',
|
||||
ai_chat_tools_mcp_tool: 'Anropade ${1}',
|
||||
ai_chat_ask: 'Fråga ${1}',
|
||||
ai_chat_ask_about_page: 'Fråga ${1} om den här sidan',
|
||||
|
||||
@@ -133,6 +133,8 @@ export const th: TranslationLanguage = {
|
||||
searched_for: 'ค้นหา ${1}',
|
||||
ai_chat_tools_listed_pages: 'เรียกดูเอกสาร',
|
||||
ai_chat_tools_read_page: 'อ่านหน้า ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'เปิดหน้าแล้ว',
|
||||
ai_chat_tools_navigate_failed: 'ไม่สามารถเปิดหน้าได้',
|
||||
ai_chat_tools_mcp_tool: 'เรียกใช้ ${1}',
|
||||
ai_chat_ask: 'ถาม ${1}',
|
||||
ai_chat_ask_about_page: 'ถาม ${1} เกี่ยวกับหน้านี้',
|
||||
|
||||
@@ -135,6 +135,8 @@ export const tr: TranslationLanguage = {
|
||||
searched_for: '${1} arandı',
|
||||
ai_chat_tools_listed_pages: 'Dokümanlara göz atıldı',
|
||||
ai_chat_tools_read_page: '${1} sayfası okundu',
|
||||
ai_chat_tools_navigated_to_page: 'Sayfa açıldı',
|
||||
ai_chat_tools_navigate_failed: 'Sayfa açılamadı',
|
||||
ai_chat_tools_mcp_tool: '${1} çağrıldı',
|
||||
ai_chat_ask: '${1} sor',
|
||||
ai_chat_ask_about_page: 'Bu sayfa hakkında ${1} sor',
|
||||
|
||||
@@ -135,6 +135,8 @@ export const uk: TranslationLanguage = {
|
||||
searched_for: 'Шукали ${1}',
|
||||
ai_chat_tools_listed_pages: 'Переглянуто документацію',
|
||||
ai_chat_tools_read_page: 'Прочитано сторінку ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Сторінку відкрито',
|
||||
ai_chat_tools_navigate_failed: 'Не вдалося відкрити сторінку',
|
||||
ai_chat_tools_mcp_tool: 'Викликано ${1}',
|
||||
ai_chat_ask: 'Запитати ${1}',
|
||||
ai_chat_ask_about_page: 'Запитати ${1} про цю сторінку',
|
||||
|
||||
@@ -135,6 +135,8 @@ export const vi: TranslationLanguage = {
|
||||
searched_for: 'Đã tìm kiếm ${1}',
|
||||
ai_chat_tools_listed_pages: 'Đã duyệt tài liệu',
|
||||
ai_chat_tools_read_page: 'Đã đọc trang ${1}',
|
||||
ai_chat_tools_navigated_to_page: 'Đã mở trang',
|
||||
ai_chat_tools_navigate_failed: 'Không thể mở trang',
|
||||
ai_chat_tools_mcp_tool: 'Đã gọi ${1}',
|
||||
ai_chat_ask: 'Hỏi ${1}',
|
||||
ai_chat_ask_about_page: 'Hỏi ${1} về trang này',
|
||||
|
||||
@@ -132,6 +132,8 @@ export const yue: TranslationLanguage = {
|
||||
searched_for: '已搜尋 ${1}',
|
||||
ai_chat_tools_listed_pages: '瀏覽咗文件',
|
||||
ai_chat_tools_read_page: '已閱讀頁面 ${1}',
|
||||
ai_chat_tools_navigated_to_page: '已開啟頁面',
|
||||
ai_chat_tools_navigate_failed: '無法開啟頁面',
|
||||
ai_chat_tools_mcp_tool: '已呼叫 ${1}',
|
||||
ai_chat_ask: '問 ${1}',
|
||||
ai_chat_ask_about_page: '問 ${1} 關於此頁面',
|
||||
|
||||
@@ -132,6 +132,8 @@ export const zh_tw: TranslationLanguage = {
|
||||
searched_for: '已搜尋 ${1}',
|
||||
ai_chat_tools_listed_pages: '瀏覽了文件',
|
||||
ai_chat_tools_read_page: '已閱讀頁面 ${1}',
|
||||
ai_chat_tools_navigated_to_page: '已開啟頁面',
|
||||
ai_chat_tools_navigate_failed: '無法開啟頁面',
|
||||
ai_chat_tools_mcp_tool: '已呼叫 ${1}',
|
||||
ai_chat_ask: '詢問 ${1}',
|
||||
ai_chat_ask_about_page: '詢問 ${1} 關於此頁面',
|
||||
|
||||
@@ -133,6 +133,8 @@ export const zh: TranslationLanguage = {
|
||||
searched_for: '搜索了 ${1}',
|
||||
ai_chat_tools_listed_pages: '浏览了文档',
|
||||
ai_chat_tools_read_page: '已读取页面 ${1}',
|
||||
ai_chat_tools_navigated_to_page: '已打开页面',
|
||||
ai_chat_tools_navigate_failed: '无法打开页面',
|
||||
ai_chat_tools_mcp_tool: '调用了 ${1}',
|
||||
ai_chat_ask: '向 ${1} 提问',
|
||||
ai_chat_ask_about_page: '向 ${1} 提问有关此页面的问题',
|
||||
|
||||
Reference in New Issue
Block a user