@stainless-api/docs 0.1.0-beta.136 → 0.1.0-beta.137
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.
- package/CHANGELOG.md +11 -0
- package/package.json +14 -14
- package/plugin/globalJs/ai-dropdown-options.ts +15 -7
- package/plugin/index.ts +5 -1
- package/plugin/loadPluginConfig.ts +1 -1
- package/plugin/specs/generateSpec.ts +1 -3
- package/plugin/vendor/preview.worker.docs.js +15523 -27699
- package/stl-docs/chat/docs-chat-handler.ts +4 -5
- package/stl-docs/chat/hook.ts +19 -9
- package/stl-docs/chat/schemas.ts +0 -43
- package/stl-docs/chat/ui/AiChat.tsx +11 -24
- package/stl-docs/chat/ui/components/ChatLog.tsx +0 -3
- package/stl-docs/chat/ui/components/MessageFeedback.tsx +1 -4
- package/stl-docs/chat/ui/components/ToolCall.tsx +20 -20
- package/stl-docs/chat/ui/types.ts +2 -2
- package/stl-docs/components/AiChatIsland.tsx +2 -6
- package/stl-docs/components/PageFrame.astro +1 -5
- package/stl-docs/fonts.ts +4 -4
- package/stl-docs/index.ts +34 -20
- package/stl-docs/loadStlDocsConfig.ts +2 -2
- package/virtual-module.d.ts +6 -1
- package/stl-docs/chat/stainless-handler/index.ts +0 -126
- package/stl-docs/chat/stream-util.ts +0 -16
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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:
|
|
11
|
+
priorMessages: { role: 'user' | 'assistant'; content: string }[];
|
|
11
12
|
},
|
|
12
13
|
abortSignal: AbortSignal,
|
|
13
14
|
) => AsyncGenerator<ResponseChunk>;
|
|
14
15
|
|
|
15
|
-
onRate
|
|
16
|
-
|
|
17
|
-
onAssignMetadata: (spanId: string, metadata: Record<string, string>) => Promise<MetadataResponseBody>;
|
|
16
|
+
onRate?: (spanId: string, score: 0 | 1) => Promise<unknown>;
|
|
18
17
|
};
|
package/stl-docs/chat/hook.ts
CHANGED
|
@@ -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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
224
|
+
return { chatMessages, sendMessage, rateMessage };
|
|
215
225
|
}
|
package/stl-docs/chat/schemas.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
21
|
+
type AssistantDoneMessage = BaseMessage & {
|
|
22
22
|
role: 'assistant';
|
|
23
23
|
respondingTo: string;
|
|
24
24
|
messageType: 'done';
|
|
25
25
|
spanId: string;
|
|
26
26
|
};
|
|
27
|
-
|
|
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
|
|
15
|
-
return <AiChat {...props}
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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,7 +18,6 @@ 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
23
|
import { getSharedLogger, setSharedLogger } from '../shared/getSharedLogger';
|
|
@@ -184,24 +184,39 @@ function stainlessDocsIntegration(
|
|
|
184
184
|
const withBase = (link: string) =>
|
|
185
185
|
/^([a-z][a-z0-9+.-]*:|\/\/)/.test(link) ? link : path.posix.join(base, link);
|
|
186
186
|
|
|
187
|
+
let vmAiChatHandlerExport = 'export const AI_CHAT_HANDLER = undefined;';
|
|
188
|
+
if (config.aiChat?.handlerEntrypoint) {
|
|
189
|
+
const rawEntrypoint = config.aiChat.handlerEntrypoint;
|
|
190
|
+
const handlerEntrypoint = rawEntrypoint.startsWith('file://')
|
|
191
|
+
? fileURLToPath(rawEntrypoint)
|
|
192
|
+
: path.isAbsolute(rawEntrypoint)
|
|
193
|
+
? rawEntrypoint
|
|
194
|
+
: path.resolve(fileURLToPath(astroConfig.root), rawEntrypoint);
|
|
195
|
+
vmAiChatHandlerExport = `export { default as AI_CHAT_HANDLER } from '${handlerEntrypoint}';`;
|
|
196
|
+
}
|
|
197
|
+
|
|
187
198
|
const virtualModules = new Map(
|
|
188
199
|
Object.entries({
|
|
189
|
-
'virtual:stl-docs-virtual-module':
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
200
|
+
'virtual:stl-docs-virtual-module': [
|
|
201
|
+
buildVirtualModuleString({
|
|
202
|
+
TABS: config.tabs.map((tab) => ({ ...tab, link: withBase(tab.link) })),
|
|
203
|
+
SPLIT_TABS_ENABLED: config.splitTabsEnabled,
|
|
204
|
+
HEADER_LINKS: config.header.links.map((link) => ({ ...link, link: withBase(link.link) })),
|
|
205
|
+
HEADER_LAYOUT: config.header.layout,
|
|
206
|
+
ENABLE_CLIENT_ROUTER: config.enableClientRouter,
|
|
207
|
+
API_REFERENCE_BASE_PATH: apiReferenceBasePath ?? '/api',
|
|
208
|
+
ENABLE_PROSE_MARKDOWN_RENDERING: config.enableProseMarkdownRendering,
|
|
209
|
+
ENABLE_CONTEXT_MENU: !!config.contextMenu, // TODO: do not duplicate this between both virtual modules
|
|
210
|
+
CONTEXT_MENU_ENABLE_THIRD_PARTY:
|
|
211
|
+
(typeof config.contextMenu === 'object' ? config.contextMenu.thirdParty : null) ?? true,
|
|
212
|
+
RENDER_PAGE_DESCRIPTIONS: config.renderPageDescriptions,
|
|
213
|
+
FONTS: getFontRoles(config.fonts),
|
|
214
|
+
LINK_GROUP_TITLES_TO_OVERVIEW_PAGES: config.linkGroupTitlesToOverviewPages,
|
|
215
|
+
RENDER_CREDITS: config.credits,
|
|
216
|
+
SITE_TITLE: config.siteTitle,
|
|
217
|
+
}),
|
|
218
|
+
].join('\n'),
|
|
219
|
+
'virtual:stl-docs-ai-chat': vmAiChatHandlerExport,
|
|
205
220
|
}),
|
|
206
221
|
);
|
|
207
222
|
|
|
@@ -216,7 +231,7 @@ function stainlessDocsIntegration(
|
|
|
216
231
|
{
|
|
217
232
|
name: 'stl-docs-virtual-modules',
|
|
218
233
|
resolveId(id) {
|
|
219
|
-
// The '\0' prefix tells Vite
|
|
234
|
+
// The '\0' prefix tells Vite "this is a virtual module" and prevents it from being resolved again.
|
|
220
235
|
if (virtualModules.has(id)) return `\0${id}`;
|
|
221
236
|
},
|
|
222
237
|
load(id) {
|
|
@@ -231,8 +246,7 @@ function stainlessDocsIntegration(
|
|
|
231
246
|
await generateExamplesPlugin({
|
|
232
247
|
projectName: config.apiReference?.stainlessProject ?? undefined,
|
|
233
248
|
logger,
|
|
234
|
-
exampleOverrides:
|
|
235
|
-
typeof config.aiChat === 'object' ? config.aiChat.exampleOverrides : undefined,
|
|
249
|
+
exampleOverrides: config.aiChat.examples,
|
|
236
250
|
}),
|
|
237
251
|
]
|
|
238
252
|
: []),
|
|
@@ -81,7 +81,7 @@ export type StainlessDocsUserConfig = {
|
|
|
81
81
|
*/
|
|
82
82
|
disableProseMarkdownRendering?: boolean;
|
|
83
83
|
disableStainlessProseIndexing?: boolean;
|
|
84
|
-
aiChat?: {
|
|
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
|
package/virtual-module.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ declare module 'virtual:stl-starlight-virtual-module' {
|
|
|
19
19
|
export const PROPERTY_SETTINGS: PropertySettingsType;
|
|
20
20
|
export const MIDDLEWARE: StlStarlightMiddleware;
|
|
21
21
|
export const ENABLE_CONTEXT_MENU: boolean;
|
|
22
|
+
export const CONTEXT_MENU_ENABLE_THIRD_PARTY: boolean;
|
|
22
23
|
export const STAINLESS_PROJECT: string | undefined;
|
|
23
24
|
export const LLMS_TXT_DESCRIPTION: string | null;
|
|
24
25
|
export const LLMS_TXT_DETAIL_THRESHOLD: number;
|
|
@@ -53,6 +54,7 @@ declare module 'virtual:stl-docs-virtual-module' {
|
|
|
53
54
|
export const API_REFERENCE_BASE_PATH: string;
|
|
54
55
|
export const ENABLE_PROSE_MARKDOWN_RENDERING: boolean;
|
|
55
56
|
export const ENABLE_CONTEXT_MENU: boolean;
|
|
57
|
+
export const CONTEXT_MENU_ENABLE_THIRD_PARTY: boolean;
|
|
56
58
|
export const RENDER_PAGE_DESCRIPTIONS: boolean;
|
|
57
59
|
export const LINK_GROUP_TITLES_TO_OVERVIEW_PAGES: boolean;
|
|
58
60
|
export const FONTS: {
|
|
@@ -63,7 +65,10 @@ declare module 'virtual:stl-docs-virtual-module' {
|
|
|
63
65
|
};
|
|
64
66
|
export const RENDER_CREDITS: boolean;
|
|
65
67
|
export const SITE_TITLE: string;
|
|
66
|
-
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
declare module 'virtual:stl-docs-ai-chat' {
|
|
71
|
+
export const AI_CHAT_HANDLER: import('./stl-docs/chat/docs-chat-handler').DocsChatHandler | undefined;
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
declare module 'virtual:stl-docs-ai-chat-examples' {
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import type { DocsLanguage } from '@stainless-api/docs-ui/routing';
|
|
2
|
-
import { JSONParser } from '@streamparser/json-whatwg';
|
|
3
|
-
import {
|
|
4
|
-
type RequestBody,
|
|
5
|
-
responseChunk,
|
|
6
|
-
type FeedbackRequestBody,
|
|
7
|
-
feedbackResponseBody,
|
|
8
|
-
type MetadataRequestBody,
|
|
9
|
-
metadataResponseBody,
|
|
10
|
-
} from '../schemas';
|
|
11
|
-
import { streamAsyncIterator } from '../stream-util';
|
|
12
|
-
import { DocsChatHandler } from '../docs-chat-handler';
|
|
13
|
-
|
|
14
|
-
const API_URL = new URL('https://app.stainless.com/api/');
|
|
15
|
-
const CHAT_ENDPOINT = new URL('ai/get-agentic-help', API_URL);
|
|
16
|
-
|
|
17
|
-
const FEEDBACK_ENDPOINT = (spanId: string) => new URL(`ai/agentic-help/${spanId}/score`, API_URL);
|
|
18
|
-
const METADATA_ENDPOINT = (spanId: string) => new URL(`ai/agentic-help/${spanId}/metadata`, API_URL);
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Identifier for tracking unique users in braintrust
|
|
22
|
-
*/
|
|
23
|
-
function getClientId() {
|
|
24
|
-
let clientId = localStorage.getItem('stainless-client-id');
|
|
25
|
-
if (!clientId) {
|
|
26
|
-
clientId = crypto.randomUUID();
|
|
27
|
-
localStorage.setItem('stainless-client-id', clientId);
|
|
28
|
-
}
|
|
29
|
-
return clientId;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Context on what the user is currently viewing to pass to the agent */
|
|
33
|
-
function getPageContext({ siteTitle }: { siteTitle: string | undefined }) {
|
|
34
|
-
const { href } = window.location;
|
|
35
|
-
const markdownUrl = `${href.replace(/\/$/, '')}/index.md`;
|
|
36
|
-
const pageTitle = document.querySelector('h1')?.textContent;
|
|
37
|
-
return [
|
|
38
|
-
`The user is viewing a documentation page${siteTitle ? ` for ${siteTitle}` : ''}.`,
|
|
39
|
-
`- Content URL: ${markdownUrl}`,
|
|
40
|
-
pageTitle && `- Page title: "${pageTitle}"`,
|
|
41
|
-
// TODO: include stainless path here? does the agent know how to use it?
|
|
42
|
-
// TODO: pass more of the page content into context without the agent having to retrieve it
|
|
43
|
-
]
|
|
44
|
-
.filter(Boolean)
|
|
45
|
-
.join('\n');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export class StainlessHandler implements DocsChatHandler {
|
|
49
|
-
constructor(
|
|
50
|
-
private language: DocsLanguage,
|
|
51
|
-
private siteTitle: string | undefined,
|
|
52
|
-
private project: string,
|
|
53
|
-
) {}
|
|
54
|
-
/**
|
|
55
|
-
* Stream chat response from the server
|
|
56
|
-
*/
|
|
57
|
-
async *generateResponse(
|
|
58
|
-
{
|
|
59
|
-
query,
|
|
60
|
-
priorMessages,
|
|
61
|
-
}: {
|
|
62
|
-
query: string;
|
|
63
|
-
priorMessages: NonNullable<RequestBody['additionalContext']>['prior_messages'];
|
|
64
|
-
},
|
|
65
|
-
abortSignal: AbortSignal,
|
|
66
|
-
) {
|
|
67
|
-
const res = await fetch(CHAT_ENDPOINT, {
|
|
68
|
-
method: 'POST',
|
|
69
|
-
headers: {
|
|
70
|
-
'Content-Type': 'application/json',
|
|
71
|
-
},
|
|
72
|
-
body: JSON.stringify({
|
|
73
|
-
query,
|
|
74
|
-
sdk: { project: this.project, language: this.language },
|
|
75
|
-
stream: true,
|
|
76
|
-
additionalContext: {
|
|
77
|
-
prior_messages: priorMessages,
|
|
78
|
-
intent: getPageContext({ siteTitle: this.siteTitle }),
|
|
79
|
-
},
|
|
80
|
-
browser_id: getClientId(),
|
|
81
|
-
} satisfies RequestBody),
|
|
82
|
-
|
|
83
|
-
signal: abortSignal,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
if (!res.ok || !res.body) throw new Error(`Chat request failed with status ${res.status}`);
|
|
87
|
-
|
|
88
|
-
const parser = new JSONParser({ separator: '\n', paths: ['$'] });
|
|
89
|
-
for await (const chunk of streamAsyncIterator(res.body.pipeThrough(parser))) {
|
|
90
|
-
const chunkParsed = responseChunk.safeParse(chunk.value);
|
|
91
|
-
if (chunkParsed.success) yield chunkParsed.data;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Attach a score to a response
|
|
97
|
-
*/
|
|
98
|
-
async onRate(spanId: string, score: 0 | 1) {
|
|
99
|
-
const res = await fetch(FEEDBACK_ENDPOINT(spanId), {
|
|
100
|
-
method: 'PUT',
|
|
101
|
-
headers: {
|
|
102
|
-
'Content-Type': 'application/json',
|
|
103
|
-
},
|
|
104
|
-
body: JSON.stringify({ score } satisfies FeedbackRequestBody),
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
if (!res.ok) throw new Error(`Feedback request failed with status ${res.status}`);
|
|
108
|
-
return feedbackResponseBody.parse(await res.json());
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Attach metadata to a response
|
|
113
|
-
*/
|
|
114
|
-
async onAssignMetadata(spanId: string, metadata: Record<string, string>) {
|
|
115
|
-
const res = await fetch(METADATA_ENDPOINT(spanId), {
|
|
116
|
-
method: 'PUT',
|
|
117
|
-
headers: {
|
|
118
|
-
'Content-Type': 'application/json',
|
|
119
|
-
},
|
|
120
|
-
body: JSON.stringify({ metadata } satisfies MetadataRequestBody),
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
if (!res.ok) throw new Error(`Metadata request failed with status ${res.status}`);
|
|
124
|
-
return metadataResponseBody.parse(await res.json());
|
|
125
|
-
}
|
|
126
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* https://jakearchibald.com/2017/async-iterators-and-generators/#making-streams-iterate
|
|
3
|
-
* safari does not yet support consuming ReadableStream as AsyncIterable
|
|
4
|
-
*/
|
|
5
|
-
export async function* streamAsyncIterator<T>(stream: ReadableStream<T>) {
|
|
6
|
-
const reader = stream.getReader();
|
|
7
|
-
try {
|
|
8
|
-
while (true) {
|
|
9
|
-
const { done, value } = await reader.read();
|
|
10
|
-
if (done) return;
|
|
11
|
-
yield value;
|
|
12
|
-
}
|
|
13
|
-
} finally {
|
|
14
|
-
reader.releaseLock();
|
|
15
|
-
}
|
|
16
|
-
}
|