@stainless-api/docs 0.1.0-beta.136 → 0.1.0-beta.138

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,23 +10,6 @@ export function buildVirtualModuleString<T extends Record<string, unknown>>(vars
10
10
  .join('\n');
11
11
  }
12
12
 
13
- export function makeVirtualModPlugin(bareId: string, content: string): VitePlugin {
14
- return {
15
- name: `stl-virtual-module-loader-${bareId}`,
16
- resolveId(id) {
17
- // The '\0' prefix tells Vite “this is a virtual module” and prevents it from being resolved again.
18
- if (id === bareId) {
19
- return `\0${bareId}`;
20
- }
21
- },
22
- load(id) {
23
- if (id === `\0${bareId}`) {
24
- return content;
25
- }
26
- },
27
- };
28
- }
29
-
30
13
  export function makeAsyncVirtualModPlugin<T extends Record<string, unknown>>(
31
14
  bareId: string,
32
15
  contentLoader: () => Promise<T | string>,
@@ -1,95 +1,31 @@
1
- import { AstroIntegrationLogger } from 'astro';
2
- import z from 'zod';
3
- import {
4
- buildVirtualModuleString,
5
- makeAsyncVirtualModPlugin,
6
- makeVirtualModPlugin,
7
- } from '../shared/virtualModule';
1
+ import { buildVirtualModuleString } from '../shared/virtualModule';
8
2
  import type * as virtualExampleModule from 'virtual:stl-docs-ai-chat-examples';
9
3
  type VirtualExampleModule = typeof virtualExampleModule;
10
4
 
11
- const exampleSchema = z.array(
12
- z.object({
13
- shortPrompt: z.string(),
14
- longPrompt: z.string(),
15
- icon: z.string(),
16
- }),
17
- );
18
- export type ExamplePromptResponse = z.infer<typeof exampleSchema>;
5
+ export type ExamplePromptResponse = {
6
+ shortPrompt: string;
7
+ longPrompt: string;
8
+ icon: string;
9
+ }[];
19
10
 
20
- // handles actually retrieving the information via the Stainless API
21
- async function loadExamples(
22
- projectName: string,
23
- logger: AstroIntegrationLogger,
24
- ): Promise<ExamplePromptResponse | undefined> {
25
- try {
26
- const response = await fetch(`https://api.stainless.com/api/ai/steelie-examples/${projectName}`, {
27
- method: 'GET',
28
- });
29
-
30
- const text = await response.text();
31
- if (!response.ok) {
32
- logger.error(`failed to fetch AI chat examples: ${text}`);
33
- return undefined;
34
- }
35
- const examples = exampleSchema.parse(JSON.parse(text));
36
- return examples;
37
- } catch (error) {
38
- if (error instanceof Error) logger.error(`failed to fetch AI chat examples: ${error.message}`);
39
- else throw error;
40
- return undefined;
41
- }
42
- }
43
-
44
- export default async function generateExamplesPlugin({
45
- projectName,
46
- logger,
47
- exampleOverrides,
48
- }: {
49
- projectName: string | undefined;
50
- logger: AstroIntegrationLogger;
51
- exampleOverrides?: ExamplePromptResponse;
52
- }) {
53
- // if the user has specified any examples, return those immediately
54
- // instead of loading them via the web.
55
- if (exampleOverrides) {
56
- return makeVirtualModPlugin(
57
- 'virtual:stl-docs-ai-chat-examples',
58
- generateVirtualModuleString(exampleOverrides),
59
- );
11
+ export function generateExamplesVirtualModule(exampleOverrides: ExamplePromptResponse | undefined): string {
12
+ if (!exampleOverrides) {
13
+ return buildVirtualModuleString({ examples: undefined } satisfies VirtualExampleModule);
60
14
  }
61
- // if we don't have a defined project name, don't try to fetch examples
62
- if (!projectName) {
63
- return makeVirtualModPlugin(
64
- 'virtual:stl-docs-ai-chat-examples',
65
- buildVirtualModuleString({ examples: undefined } satisfies VirtualExampleModule),
66
- );
67
- }
68
-
69
- // otherwise, promise to get the right examples at some point later on
70
- const examplesPromise = loadExamples(projectName, logger);
71
- return makeAsyncVirtualModPlugin<VirtualExampleModule>('virtual:stl-docs-ai-chat-examples', async () => {
72
- const examples = await examplesPromise;
73
- return generateVirtualModuleString(examples);
74
- });
75
- }
76
-
77
- function generateVirtualModuleString(examples: ExamplePromptResponse | undefined) {
78
- if (!examples) return 'export const examples = undefined;';
79
15
 
80
16
  // Generate icon imports
81
17
  // prettier-ignore
82
18
  const pascalToKebab = (str: string) => str.split(/(?=[A-Z])/).join('-').toLowerCase();
83
19
  const iconImportPath = (iconName: string) =>
84
20
  import.meta.resolve(`lucide-react/dist/esm/icons/${pascalToKebab(iconName)}.js`);
85
- const iconImports = examples.map(
21
+ const iconImports = exampleOverrides.map(
86
22
  ({ icon }) => `import ${icon} from ${JSON.stringify(iconImportPath(icon))}`,
87
23
  );
88
24
 
89
25
  // Reference icon imports in `examples` exported object
90
26
  // "icon":"Sparkles" -> "icon":Sparkles
91
27
  const iconStringsToIdents = (jsonBlob: string) => jsonBlob.replace(/"icon":\s*"(\w+)"/g, '"icon":$1');
92
- const exportBody = `export const examples = ${iconStringsToIdents(JSON.stringify(examples))};`;
28
+ const exportBody = `export const examples = ${iconStringsToIdents(JSON.stringify(exampleOverrides))};`;
93
29
 
94
30
  return [...iconImports, exportBody].join('\n');
95
31
  }
@@ -1,4 +1,5 @@
1
- import { FeedbackResponseBody, MetadataResponseBody, RequestBody, ResponseChunk } from './schemas';
1
+ import { ResponseChunk } from './schemas';
2
+ export { responseChunk, type ResponseChunk } from './schemas';
2
3
 
3
4
  export type DocsChatHandler = {
4
5
  generateResponse: (
@@ -7,12 +8,10 @@ export type DocsChatHandler = {
7
8
  priorMessages,
8
9
  }: {
9
10
  query: string;
10
- priorMessages: NonNullable<RequestBody['additionalContext']>['prior_messages'];
11
+ priorMessages: { role: 'user' | 'assistant'; content: string }[];
11
12
  },
12
13
  abortSignal: AbortSignal,
13
14
  ) => AsyncGenerator<ResponseChunk>;
14
15
 
15
- onRate: (spanId: string, score: 0 | 1) => Promise<FeedbackResponseBody>;
16
-
17
- onAssignMetadata: (spanId: string, metadata: Record<string, string>) => Promise<MetadataResponseBody>;
16
+ onRate?: (spanId: string, score: 0 | 1) => Promise<unknown>;
18
17
  };
@@ -129,6 +129,7 @@ export function useChat({ handler }: { handler: DocsChatHandler }) {
129
129
 
130
130
  try {
131
131
  let chunk: ResponseChunk | undefined = undefined;
132
+ let sawDone = false;
132
133
  for await (chunk of handler.generateResponse(
133
134
  {
134
135
  query: question,
@@ -147,6 +148,7 @@ export function useChat({ handler }: { handler: DocsChatHandler }) {
147
148
 
148
149
  if (chunk.type === 'done') {
149
150
  dispatch({ type: 'completeResponse', respondingTo: userMessageId, spanId: chunk.span_id });
151
+ sawDone = true;
150
152
  // stop reading from the stream on done
151
153
  break;
152
154
  }
@@ -190,6 +192,12 @@ export function useChat({ handler }: { handler: DocsChatHandler }) {
190
192
  respondingTo: userMessageId,
191
193
  errorMessage: 'No response received. Please try again.',
192
194
  });
195
+ } else if (!sawDone && !abortControllerRef.current.signal.aborted) {
196
+ // Generator exhausted without a `done` chunk — synthesize completion.
197
+ if (lastChunkType === 'text') {
198
+ dispatch({ type: 'completeMessage', id: currentResponseId });
199
+ }
200
+ dispatch({ type: 'completeResponse', respondingTo: userMessageId, spanId: crypto.randomUUID() });
193
201
  }
194
202
  } catch {
195
203
  dispatch({
@@ -202,14 +210,16 @@ export function useChat({ handler }: { handler: DocsChatHandler }) {
202
210
  [chatMessages, handler],
203
211
  );
204
212
 
205
- async function rateMessage(spanId: string, rating: 'up' | 'down'): Promise<boolean> {
206
- try {
207
- const { success } = await handler.onRate(spanId, { up: 1 as const, down: 0 as const }[rating]);
208
- return success;
209
- } catch {
210
- return false;
211
- }
212
- }
213
+ const rateMessage = handler.onRate
214
+ ? async (spanId: string, rating: 'up' | 'down') => {
215
+ try {
216
+ await handler.onRate?.(spanId, { up: 1 as const, down: 0 as const }[rating]);
217
+ return true;
218
+ } catch {
219
+ return false;
220
+ }
221
+ }
222
+ : undefined;
213
223
 
214
- return { chatMessages, sendMessage, rateMessage, setMetadata: handler.onAssignMetadata.bind(handler) };
224
+ return { chatMessages, sendMessage, rateMessage };
215
225
  }
@@ -1,38 +1,5 @@
1
1
  import z from 'zod';
2
2
 
3
- // TODO: replace with generated SDK types instead of copy/pasting from other repo
4
- export const requestBody = z.object({
5
- query: z.string(),
6
- sdk: z.object({
7
- project: z.string(),
8
- language: z.string(),
9
- version: z.string().optional(),
10
- }),
11
- stream: z.boolean().optional(),
12
- budget: z
13
- .object({
14
- maxTokens: z.number().optional(),
15
- })
16
- .optional(),
17
- session_id: z.string().optional(),
18
- additionalContext: z
19
- .object({
20
- prior_messages: z.array(
21
- z.object({
22
- role: z.enum(['user', 'assistant']),
23
- content: z.string(),
24
- }),
25
- ),
26
- code: z.string().optional(),
27
- intent: z.string().optional(),
28
- lsp: z.string().optional(),
29
- errors: z.string().optional(),
30
- })
31
- .optional(),
32
- browser_id: z.string().optional(),
33
- });
34
- export type RequestBody = z.input<typeof requestBody>;
35
-
36
3
  export const responseChunk = z.discriminatedUnion('type', [
37
4
  z.object({
38
5
  type: z.literal('text'),
@@ -58,13 +25,3 @@ export const responseChunk = z.discriminatedUnion('type', [
58
25
  }),
59
26
  ]);
60
27
  export type ResponseChunk = z.infer<typeof responseChunk>;
61
-
62
- export const feedbackRequestBody = z.object({ score: z.number().min(0).max(1) });
63
- export type FeedbackRequestBody = z.infer<typeof feedbackRequestBody>;
64
- export const feedbackResponseBody = z.object({ success: z.boolean() });
65
- export type FeedbackResponseBody = z.infer<typeof feedbackResponseBody>;
66
-
67
- export const metadataRequestBody = z.object({ metadata: z.record(z.string(), z.string()) });
68
- export type MetadataRequestBody = z.infer<typeof metadataRequestBody>;
69
- export const metadataResponseBody = z.object({ success: z.boolean() });
70
- export type MetadataResponseBody = z.infer<typeof metadataResponseBody>;
@@ -2,13 +2,13 @@ import { motion } from 'motion/react';
2
2
  import { Suspense, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
3
3
  import { useScrollToBottom } from './scroll-manager';
4
4
  import { useChat } from '../hook';
5
- import { DocsLanguage } from '@stainless-api/docs-ui/routing';
5
+ import type { DocsChatHandler } from '../docs-chat-handler';
6
6
 
7
7
  import ChatLog from './components/ChatLog';
8
8
  import AiChatTrigger from './Trigger';
9
9
  import ChatEmpty from './components/ChatEmpty';
10
10
  import ChatControls from './components/ChatControls';
11
- import { StainlessHandler } from '../stainless-handler';
11
+ import { AI_CHAT_HANDLER } from 'virtual:stl-docs-ai-chat';
12
12
 
13
13
  import styles from './AiChat.module.css';
14
14
  import clsx from 'clsx';
@@ -26,30 +26,18 @@ const examplesPromise = import('virtual:stl-docs-ai-chat-examples')
26
26
  .then((mod) => mod.examples)
27
27
  .catch(() => undefined);
28
28
 
29
- export default function AiChat({
30
- stainlessProject,
31
- currentLanguage,
32
- siteTitle,
33
- }: {
34
- stainlessProject: string;
35
- currentLanguage?: DocsLanguage;
36
- siteTitle?: string;
37
- }) {
38
- const handler = useMemo(
39
- () => new StainlessHandler(currentLanguage ?? 'http', siteTitle, stainlessProject),
40
- [currentLanguage, siteTitle, stainlessProject],
41
- );
42
- const { chatMessages, sendMessage, rateMessage, setMetadata } = useChat({
29
+ export default function AiChat({ siteTitle }: { siteTitle?: string }) {
30
+ if (!AI_CHAT_HANDLER) {
31
+ return null;
32
+ }
33
+ return <AiChatInner siteTitle={siteTitle} handler={AI_CHAT_HANDLER} />;
34
+ }
35
+
36
+ function AiChatInner({ siteTitle, handler }: { siteTitle?: string; handler: DocsChatHandler }) {
37
+ const { chatMessages, sendMessage, rateMessage } = useChat({
43
38
  handler,
44
39
  });
45
40
 
46
- const onCopyMessage = useCallback(
47
- (spanId: string) => {
48
- setMetadata(spanId, { copied_to_clipboard: 'true' }).catch(() => {});
49
- },
50
- [setMetadata],
51
- );
52
-
53
41
  // panel mode is supported only on larger viewports
54
42
  const supportsPanel = useSyncExternalStore(
55
43
  (cb) => {
@@ -168,7 +156,6 @@ export default function AiChat({
168
156
  <ChatLog
169
157
  messages={chatMessages}
170
158
  rateMessage={rateMessage}
171
- onCopyMessage={onCopyMessage}
172
159
  responsePending={pendingResponses > 0}
173
160
  />
174
161
  ) : (
@@ -11,12 +11,10 @@ import { LoaderCircleIcon } from 'lucide-react';
11
11
  export default function ChatLog({
12
12
  messages,
13
13
  rateMessage,
14
- onCopyMessage,
15
14
  responsePending = false,
16
15
  }: {
17
16
  messages: ChatMessage[];
18
17
  rateMessage?: (spanId: string, rating: 'up' | 'down') => Promise<boolean>;
19
- onCopyMessage?: (spanId: string) => void;
20
18
  responsePending?: boolean;
21
19
  }) {
22
20
  const lastMessage = messages.at(-1);
@@ -57,7 +55,6 @@ export default function ChatLog({
57
55
  key={msg.id}
58
56
  spanId={msg.spanId}
59
57
  rateMessage={rateMessage}
60
- onCopyMessage={onCopyMessage}
61
58
  // all "text" responses to the given message
62
59
  messages={messages.flatMap((msg2) =>
63
60
  msg2.role === 'assistant' &&
@@ -12,17 +12,14 @@ export default function MessageFeedbackButtons({
12
12
  spanId,
13
13
  messages,
14
14
  rateMessage,
15
- onCopyMessage,
16
15
  }: {
17
16
  spanId: string;
18
17
  messages: AssistantTextMessage[];
19
18
  rateMessage?: (spanId: string, rating: 'up' | 'down') => Promise<boolean>;
20
- onCopyMessage?: (spanId: string) => void;
21
19
  }) {
22
20
  // Copy response as markdown
23
21
  const [copied, setCopied] = useState(false);
24
22
  const handleCopy = useCallback(() => {
25
- onCopyMessage?.(spanId);
26
23
  const combinedText = messages.map((msg) => msg.content).join('\n\n');
27
24
  navigator.clipboard
28
25
  .writeText(combinedText)
@@ -33,7 +30,7 @@ export default function MessageFeedbackButtons({
33
30
  .catch(() => {
34
31
  setCopied(false);
35
32
  });
36
- }, [messages, spanId, onCopyMessage]);
33
+ }, [messages]);
37
34
 
38
35
  // Provide message rating
39
36
  const [rating, setRating] = useState<'up' | 'down' | null>(null);
@@ -1,5 +1,4 @@
1
1
  import { motion } from 'motion/react';
2
- import z from 'zod';
3
2
 
4
3
  import type { AssistantToolCallMessage } from '../types';
5
4
 
@@ -11,24 +10,25 @@ export default function ToolCall({
11
10
  }: {
12
11
  message: Pick<AssistantToolCallMessage, 'id' | 'toolName' | 'input'>;
13
12
  }) {
14
- // Render docs searches
15
- if (message.toolName.endsWith('search_docs')) {
16
- const parsed = z.object({ query: z.string() }).safeParse(message.input);
17
- if (parsed.success) {
18
- return (
19
- <motion.li
20
- layout="position"
21
- data-message-role="assistant"
22
- className={clsx(styles['chat-message'], styles['tool-use'])}
23
- >
24
- <p>
25
- Fetching docs for <em>{parsed.data.query}</em>
26
- </p>
27
- </motion.li>
28
- );
29
- }
30
- }
13
+ const firstStringArg = message.input
14
+ ? Object.values(message.input).find((v): v is string => typeof v === 'string')
15
+ : undefined;
31
16
 
32
- // No other tool renderers yet
33
- return null;
17
+ return (
18
+ <motion.li
19
+ layout="position"
20
+ data-message-role="assistant"
21
+ className={clsx(styles['chat-message'], styles['tool-use'])}
22
+ >
23
+ <p>
24
+ Calling <code>{message.toolName}</code>
25
+ {firstStringArg && (
26
+ <>
27
+ {' '}
28
+ with <em>{firstStringArg}</em>
29
+ </>
30
+ )}
31
+ </p>
32
+ </motion.li>
33
+ );
34
34
  }
@@ -18,13 +18,13 @@ export type AssistantToolCallMessage = BaseMessage & {
18
18
  toolName: string;
19
19
  input: Record<string, unknown> | undefined;
20
20
  };
21
- export type AssistantDoneMessage = BaseMessage & {
21
+ type AssistantDoneMessage = BaseMessage & {
22
22
  role: 'assistant';
23
23
  respondingTo: string;
24
24
  messageType: 'done';
25
25
  spanId: string;
26
26
  };
27
- export type AssistantErrorMessage = BaseMessage & {
27
+ type AssistantErrorMessage = BaseMessage & {
28
28
  role: 'assistant';
29
29
  respondingTo: string;
30
30
  messageType: 'error';
@@ -4,13 +4,9 @@
4
4
  // This conditional can’t be inlined into PageFrame because it breaks Astro’s static analysis of imports of client islands
5
5
  const AiChat = __STLDOCS_ENABLE_AI_CHAT__ ? (await import('../chat/ui/AiChat')).default : null;
6
6
 
7
- const STAINLESS_PROJECT = __STLDOCS_HAS_API_REFERENCE__
8
- ? (await import('virtual:stl-starlight-virtual-module')).STAINLESS_PROJECT
9
- : undefined;
10
-
11
7
  export default function DocsChatLazy(
12
8
  props: Omit<React.ComponentProps<NonNullable<typeof AiChat>>, 'stainlessProject'>,
13
9
  ) {
14
- if (!AiChat || !STAINLESS_PROJECT) return null;
15
- return <AiChat {...props} stainlessProject={STAINLESS_PROJECT} />;
10
+ if (!AiChat) return null;
11
+ return <AiChat {...props} />;
16
12
  }
@@ -29,9 +29,5 @@ const { hasSidebar } = Astro.locals.starlightRoute;
29
29
 
30
30
  <slot />
31
31
 
32
- {
33
- __STLDOCS_ENABLE_AI_CHAT__ && (
34
- <AiChatIsland client:load currentLanguage={Astro.locals.language} siteTitle={siteTitle} />
35
- )
36
- }
32
+ {__STLDOCS_ENABLE_AI_CHAT__ && <AiChatIsland client:load siteTitle={siteTitle} />}
37
33
  </div>
package/stl-docs/fonts.ts CHANGED
@@ -7,7 +7,7 @@ type AstroFontConfigEntry = Defined<AstroConfig['fonts']>[number];
7
7
 
8
8
  // Apply Omit to each member of the union while preserving union structure
9
9
  type PreloadFilter = { preload?: FontPreloadFilter };
10
- export type StlDocsFontConfigEntry = (AstroFontConfigEntry extends infer T
10
+ type StlDocsFontConfigEntry = (AstroFontConfigEntry extends infer T
11
11
  ? T extends unknown
12
12
  ? Omit<T, 'cssVariable'>
13
13
  : never
@@ -165,19 +165,19 @@ export function flattenFonts(fonts: StlDocsFontConfig | undefined): AstroFontCon
165
165
  fontConfigs.push({
166
166
  ...fonts.primary,
167
167
  cssVariable: '--stl-typography-font' as const,
168
- } as AstroFontConfigEntry);
168
+ });
169
169
  }
170
170
  if (fonts.heading) {
171
171
  fontConfigs.push({
172
172
  ...fonts.heading,
173
173
  cssVariable: '--stl-typography-font-heading' as const,
174
- } as AstroFontConfigEntry);
174
+ });
175
175
  }
176
176
  if (fonts.mono) {
177
177
  fontConfigs.push({
178
178
  ...fonts.mono,
179
179
  cssVariable: '--stl-typography-font-mono' as const,
180
- } as AstroFontConfigEntry);
180
+ });
181
181
  }
182
182
  if (fonts.additional) {
183
183
  fontConfigs.push(...fonts.additional.map((font) => font as AstroFontConfigEntry));
package/stl-docs/index.ts CHANGED
@@ -7,6 +7,7 @@ import type { AstroIntegration } from 'astro';
7
7
 
8
8
  import { normalizeRedirects, type NormalizedRedirectConfig } from './redirects';
9
9
  import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
10
11
  import { mkdirSync, writeFileSync } from 'fs';
11
12
  import {
12
13
  parseStlDocsConfig,
@@ -17,16 +18,14 @@ import {
17
18
  type StarlightSidebarConfig,
18
19
  } from './loadStlDocsConfig';
19
20
  import { buildVirtualModuleString } from '../shared/virtualModule';
20
- import type * as StlDocsVirtualModule from 'virtual:stl-docs-virtual-module';
21
21
  import { resolveSrcFile } from '../resolveSrcFile';
22
22
  import { stainlessDocsMarkdownRenderer } from './proseMarkdown/proseMarkdownIntegration';
23
- import { getSharedLogger, setSharedLogger } from '../shared/getSharedLogger';
23
+ import { setSharedLogger } from '../shared/getSharedLogger';
24
24
  import { stainlessDocsVectorProseIndexing } from './proseDocSync';
25
- import { stainlessDocsAlgoliaProseIndexing } from './proseSearchIndexing';
26
25
  import { stainlessStarlight } from '../plugin';
27
26
  import { getFontRoles, flattenFonts } from './fonts';
28
27
  import conditionalIntegration from '../shared/conditionalIntegration';
29
- import generateExamplesPlugin from './aiChatExamples';
28
+ import { generateExamplesVirtualModule } from './aiChatExamples';
30
29
  import { ogImageStarlightPlugin } from './og-image';
31
30
 
32
31
  export * from '../plugin';
@@ -172,8 +171,7 @@ function stainlessDocsIntegration(
172
171
  return {
173
172
  name: 'stl-docs-astro',
174
173
  hooks: {
175
- 'astro:config:setup': async ({ updateConfig, command, config: astroConfig, logger: localLogger }) => {
176
- const logger = getSharedLogger({ fallback: localLogger });
174
+ 'astro:config:setup': ({ updateConfig, command, config: astroConfig }) => {
177
175
  // we only handle redirects for builds
178
176
  // in dev, Astro handles them for us
179
177
  if (command === 'build' && astroConfig.redirects) {
@@ -184,9 +182,21 @@ function stainlessDocsIntegration(
184
182
  const withBase = (link: string) =>
185
183
  /^([a-z][a-z0-9+.-]*:|\/\/)/.test(link) ? link : path.posix.join(base, link);
186
184
 
187
- const virtualModules = new Map(
188
- Object.entries({
189
- 'virtual:stl-docs-virtual-module': buildVirtualModuleString({
185
+ let vmAiChatHandlerExport = 'export const AI_CHAT_HANDLER = undefined;';
186
+ if (config.aiChat?.handlerEntrypoint) {
187
+ const rawEntrypoint = config.aiChat.handlerEntrypoint;
188
+ const handlerEntrypoint = rawEntrypoint.startsWith('file://')
189
+ ? fileURLToPath(rawEntrypoint)
190
+ : path.isAbsolute(rawEntrypoint)
191
+ ? rawEntrypoint
192
+ : path.resolve(fileURLToPath(astroConfig.root), rawEntrypoint);
193
+ vmAiChatHandlerExport = `export { default as AI_CHAT_HANDLER } from '${handlerEntrypoint}';`;
194
+ }
195
+
196
+ const virtualModules = new Map<string, string>([
197
+ [
198
+ 'virtual:stl-docs-virtual-module',
199
+ buildVirtualModuleString({
190
200
  TABS: config.tabs.map((tab) => ({ ...tab, link: withBase(tab.link) })),
191
201
  SPLIT_TABS_ENABLED: config.splitTabsEnabled,
192
202
  HEADER_LINKS: config.header.links.map((link) => ({ ...link, link: withBase(link.link) })),
@@ -194,16 +204,24 @@ function stainlessDocsIntegration(
194
204
  ENABLE_CLIENT_ROUTER: config.enableClientRouter,
195
205
  API_REFERENCE_BASE_PATH: apiReferenceBasePath ?? '/api',
196
206
  ENABLE_PROSE_MARKDOWN_RENDERING: config.enableProseMarkdownRendering,
197
- ENABLE_CONTEXT_MENU: config.contextMenu, // TODO: do not duplicate this between both virtual modules
207
+ ENABLE_CONTEXT_MENU: !!config.contextMenu, // TODO: do not duplicate this between both virtual modules
208
+ CONTEXT_MENU_ENABLE_THIRD_PARTY:
209
+ (typeof config.contextMenu === 'object' ? config.contextMenu.thirdParty : null) ?? true,
198
210
  RENDER_PAGE_DESCRIPTIONS: config.renderPageDescriptions,
199
211
  FONTS: getFontRoles(config.fonts),
200
212
  LINK_GROUP_TITLES_TO_OVERVIEW_PAGES: config.linkGroupTitlesToOverviewPages,
201
213
  RENDER_CREDITS: config.credits,
202
214
  SITE_TITLE: config.siteTitle,
203
- ENABLE_AI_CHAT: !!config.aiChat,
204
- } satisfies typeof StlDocsVirtualModule),
205
- }),
206
- );
215
+ }),
216
+ ],
217
+ ['virtual:stl-docs-ai-chat', vmAiChatHandlerExport],
218
+ ]);
219
+ if (config.aiChat) {
220
+ virtualModules.set(
221
+ 'virtual:stl-docs-ai-chat-examples',
222
+ generateExamplesVirtualModule(config.aiChat.examples),
223
+ );
224
+ }
207
225
 
208
226
  updateConfig({
209
227
  fonts: [...flattenFonts(config.fonts), ...(astroConfig?.fonts ?? [])],
@@ -216,7 +234,7 @@ function stainlessDocsIntegration(
216
234
  {
217
235
  name: 'stl-docs-virtual-modules',
218
236
  resolveId(id) {
219
- // The '\0' prefix tells Vite this is a virtual module and prevents it from being resolved again.
237
+ // The '\0' prefix tells Vite "this is a virtual module" and prevents it from being resolved again.
220
238
  if (virtualModules.has(id)) return `\0${id}`;
221
239
  },
222
240
  load(id) {
@@ -224,18 +242,6 @@ function stainlessDocsIntegration(
224
242
  if (virtualModules.has(bare)) return virtualModules.get(bare);
225
243
  },
226
244
  },
227
- // Separate plugin for the examples because it has async resolution; not a simple string
228
- // like the above plugins
229
- ...(config.aiChat
230
- ? [
231
- await generateExamplesPlugin({
232
- projectName: config.apiReference?.stainlessProject ?? undefined,
233
- logger,
234
- exampleOverrides:
235
- typeof config.aiChat === 'object' ? config.aiChat.exampleOverrides : undefined,
236
- }),
237
- ]
238
- : []),
239
245
  ],
240
246
  },
241
247
  build: {
@@ -297,11 +303,6 @@ export function stainlessDocs(config: StainlessDocsUserConfig): AstroIntegration
297
303
  integration: stainlessDocsMarkdownRenderer({ apiReferenceBasePath }),
298
304
  reason: 'disabled by experimental config "disableProseMarkdownRendering"',
299
305
  }),
300
- conditionalIntegration({
301
- condition: !config.experimental?.disableStainlessProseIndexing,
302
- integration: stainlessDocsAlgoliaProseIndexing({ apiReferenceBasePath }),
303
- reason: 'disabled by experimental config "disableStainlessProseIndexing"',
304
- }),
305
306
  conditionalIntegration({
306
307
  condition: !config.experimental?.disableStainlessProseIndexing,
307
308
  integration: stainlessDocsVectorProseIndexing(normalizedConfig, apiReferenceBasePath),
@@ -81,7 +81,7 @@ export type StainlessDocsUserConfig = {
81
81
  */
82
82
  disableProseMarkdownRendering?: boolean;
83
83
  disableStainlessProseIndexing?: boolean;
84
- aiChat?: { exampleOverrides?: ExamplePromptResponse } | true;
84
+ aiChat?: { handlerEntrypoint: string; examples?: ExamplePromptResponse };
85
85
  /**
86
86
  * Whether to link group titles to overview pages. Note: overview pages must already be present in the sidebar for this to work.
87
87
  *
@@ -94,7 +94,7 @@ export type StainlessDocsUserConfig = {
94
94
  *
95
95
  * @default true
96
96
  */
97
- contextMenu?: boolean;
97
+ contextMenu?: boolean | { thirdParty?: boolean };
98
98
 
99
99
  /**
100
100
  * Whether to render page descriptions in prose page headers