Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/openapi-custom-language-rewrite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@gitbook/react-openapi": minor
"gitbook": patch
---

OpenAPI code samples: add a "Custom" option to the language dropdown that opens the assistant pre-filled (as a draft) to rewrite the request in any language, and show a language icon next to every option.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use client';

import { useAIChatController, useAIConfig } from '@/components/AI';
import { AIChatIcon, getAIChatName } from '@/components/AIChat';
import { useLanguage } from '@/intl/client';
import { CustomizationAIMode } from '@gitbook/api';
import {
type OpenAPICodeSampleAssistant,
OpenAPICodeSampleAssistantProvider,
} from '@gitbook/react-openapi';
import { useMemo } from 'react';

/**
* Bridge the GitBook AI assistant into the OpenAPI code sample selector, so it can
* offer a "Custom" option that opens the assistant pre-filled to rewrite a sample.
*
* When the assistant is not enabled, the provider passes `null` and the option is hidden.
*/
export function OpenAPICodeSampleAIProvider(props: { children: React.ReactNode }) {
const { children } = props;
const config = useAIConfig();
const language = useLanguage();
const chatController = useAIChatController();

const assistant = useMemo<OpenAPICodeSampleAssistant | null>(() => {
if (config.aiMode !== CustomizationAIMode.Assistant) {
return null;
}

return {
label: config.assistantName ?? getAIChatName(language, config.trademark),
icon: (
<AIChatIcon
state="default"
trademark={config.trademark}
className="me-1.5 size-4 shrink-0"
/>
),
onRewrite: ({ id, code, syntax, label, prompt }) => {
if (!code.trim()) {
return;
}
chatController.addReference({
type: 'code-block',
id,
label: label ?? 'Code',
content: code,
syntax,
});
chatController.setDraft(prompt);
chatController.open();
chatController.focus();
},
};
}, [config.aiMode, config.assistantName, config.trademark, language, chatController]);

return (
<OpenAPICodeSampleAssistantProvider value={assistant}>
{children}
</OpenAPICodeSampleAssistantProvider>
);
}
49 changes: 48 additions & 1 deletion packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { JSONDocument } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import { Icon, type IconName } from '@gitbook/icons';
import { type OpenAPIContextInput, checkIsValidLocale } from '@gitbook/react-openapi';

import type { BlockProps } from '../Block';
Expand Down Expand Up @@ -73,6 +73,9 @@ export function getOpenAPIContext(args: {
blockStyle="max-w-full"
/>
),
getCodeSampleIcon: (sample) => (
<Icon icon={getCodeSampleIconName(sample)} className="me-1.5 size-4 shrink-0" />
),
renderHeading: (headingProps) => (
<Heading
document={props.document}
Expand Down Expand Up @@ -104,3 +107,47 @@ export function getOpenAPIContext(args: {
locale,
};
}

/**
* Resolve the icon shown next to a code sample language in the selector.
* Falls back to a generic code icon for unknown languages.
*/
function getCodeSampleIconName(sample: { id?: string; syntax: string; label: string }): IconName {
const key = (sample.id ?? sample.syntax).toLowerCase();
switch (key) {
case 'http':
return 'globe';
case 'curl':
case 'bash':
case 'sh':
case 'shell':
case 'zsh':
return 'square-terminal';
case 'javascript':
case 'js':
case 'jsx':
case 'mjs':
case 'cjs':
case 'node':
return 'js';
case 'python':
case 'py':
return 'python';
case 'go':
case 'golang':
return 'golang';
case 'rust':
case 'rs':
return 'rust';
case 'php':
return 'php';
case 'java':
return 'java';
case 'swift':
return 'swift';
case 'json':
return 'brackets-curly';
default:
return 'code';
}
}
10 changes: 10 additions & 0 deletions packages/gitbook/src/components/DocumentView/OpenAPI/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,11 @@ button.openapi-mcp {
@apply flex flex-row items-center;
}

/* Single code sample (no dropdown): icon next to the language label. */
.openapi-codesample-label {
@apply flex items-center;
}

.openapi-response-media-types-examples-footer-content {
@apply flex flex-row items-center gap-2.5;
}
Expand Down Expand Up @@ -696,6 +701,11 @@ body:has(.openapi-select-popover) {
@apply flex flex-col gap-1 justify-start items-start;
}

/* The "Custom" AI rewrite option: assistant icon next to a stacked title + description. */
.openapi-select-item-custom .openapi-select-item-text {
@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;
Expand Down
5 changes: 4 additions & 1 deletion packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { RenderAIMessageOptions } from '../AI';
import { AIChat, AskAITextSelection } from '../AIChat';
import { AdaptiveVisitorContextProvider } from '../Adaptive';
import { Announcement } from '../Announcement';
import { OpenAPICodeSampleAIProvider } from '../DocumentView/OpenAPI/OpenAPICodeSampleAIProvider';
import { SpacesDropdown, TranslationsDropdown } from '../Header/SpacesDropdown';
import { InsightsProvider, VisitorProvider } from '../Insights';
import { SearchContainer, getSearchBaseProps } from '../Search';
Expand Down Expand Up @@ -92,7 +93,9 @@ export function SpaceLayoutServerContext(props: SpaceLayoutProps) {
>
<InsightsProvider enabled={withTracking} eventUrl={eventUrl.toString()}>
<AIChatProvider renderMessageOptions={aiChatRenderMessageOptions}>
{children}
<OpenAPICodeSampleAIProvider>
{children}
</OpenAPICodeSampleAIProvider>
</AIChatProvider>
</InsightsProvider>
</VisitorProvider>
Expand Down
16 changes: 16 additions & 0 deletions packages/react-openapi/src/OpenAPICodeSample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,18 @@ function generateCodeSamples(props: {
);

return codeSampleGenerators.map((generator) => {
const icon = context.getCodeSampleIcon?.({
id: generator.id,
syntax: generator.syntax,
label: generator.label,
});
if (mediaTypeRendererFactories.length > 0) {
const renderers = mediaTypeRendererFactories.map((generate) => generate(generator));
return {
key: `default-${generator.id}`,
label: generator.label,
icon,
syntax: generator.syntax,
body: (
<OpenAPIMediaTypeExamplesBody
method={data.method}
Expand All @@ -186,6 +193,8 @@ function generateCodeSamples(props: {
return {
key: `default-${generator.id}`,
label: generator.label,
icon,
syntax: generator.syntax,
body: context.renderCodeBlock({
code: generator.generate({
url: { origin: serverUrlOrigin, path },
Expand Down Expand Up @@ -299,6 +308,8 @@ function getCustomCodeSamples(props: {
let customCodeSamples: null | Array<{
key: string;
label: string;
icon?: React.ReactNode;
syntax?: string;
body: React.ReactNode;
}> = null;

Expand All @@ -312,6 +323,11 @@ function getCustomCodeSamples(props: {
.map((sample, index) => ({
key: `custom-sample-${sample.lang}-${index}`,
label: sample.label || sample.lang,
icon: context.getCodeSampleIcon?.({
syntax: sample.lang,
label: sample.label || sample.lang,
}),
syntax: sample.lang,
body: context.renderCodeBlock({
code: sample.source,
syntax: sample.lang,
Expand Down
59 changes: 59 additions & 0 deletions packages/react-openapi/src/OpenAPICodeSampleAssistant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use client';

import { createContext, useContext } from 'react';

/**
* A request to rewrite a code sample with the assistant.
*/
export interface OpenAPICodeSampleRewriteInput {
/** Stable identifier of the code sample, used to stage the reference. */
id: string;
/** Source code of the currently displayed code sample. */
code: string;
/** Syntax/language of the code sample (e.g. `bash`, `python`). */
syntax?: string;
/** Human-readable label of the code sample (e.g. `cURL`). */
label?: string;
/** Localized prompt to pre-fill in the assistant input as a draft. */
prompt: string;
}

/**
* Capability provided by the host app to let the assistant rewrite a code sample.
* When present, the code sample selector exposes a "Custom" option.
*/
export interface OpenAPICodeSampleAssistant {
/** Display name of the assistant (e.g. `GitBook Assistant`). */
label: string;
/** Logo/icon of the assistant, displayed next to the "Custom" option. */
icon: React.ReactNode;
/**
* Open the assistant with the given code sample staged as a reference and the
* prompt pre-filled as a draft (not sent).
*/
onRewrite: (input: OpenAPICodeSampleRewriteInput) => void;
}

const OpenAPICodeSampleAssistantContext = createContext<OpenAPICodeSampleAssistant | null>(null);

/**
* Provide the assistant capability to the code sample selector.
* Pass `null` to disable the "Custom" rewrite option.
*/
export function OpenAPICodeSampleAssistantProvider(props: {
value: OpenAPICodeSampleAssistant | null;
children: React.ReactNode;
}) {
return (
<OpenAPICodeSampleAssistantContext.Provider value={props.value}>
{props.children}
</OpenAPICodeSampleAssistantContext.Provider>
);
}

/**
* Access the assistant capability, or `null` when no assistant is available.
*/
export function useOpenAPICodeSampleAssistant() {
return useContext(OpenAPICodeSampleAssistantContext);
}
Loading
Loading