@stainless-api/docs 0.1.0-beta.15 → 0.1.0-beta.17
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 +15 -0
- package/package.json +9 -17
- package/plugin/buildAlgoliaIndex.ts +28 -3
- package/plugin/cms/server.ts +95 -52
- package/plugin/cms/sidebar-builder.ts +0 -9
- package/plugin/globalJs/ai-dropdown-options.ts +161 -0
- package/plugin/globalJs/navigation.ts +0 -22
- package/plugin/index.ts +46 -17
- package/plugin/loadPluginConfig.ts +90 -21
- package/plugin/react/Routing.tsx +13 -23
- package/plugin/routes/Docs.astro +7 -16
- package/plugin/routes/Overview.astro +7 -5
- package/plugin/vendor/preview.worker.docs.js +6357 -6132
- package/resolveSrcFile.ts +10 -0
- package/shared/getSharedLogger.ts +15 -0
- package/shared/terminalUtils.ts +3 -0
- package/stl-docs/components/AIDropdown.tsx +52 -0
- package/stl-docs/components/Head.astro +9 -0
- package/stl-docs/components/PageTitle.astro +67 -0
- package/stl-docs/components/content-panel/ContentPanel.astro +8 -35
- package/stl-docs/components/icons/chat-gpt.tsx +1 -1
- package/stl-docs/components/icons/markdown.tsx +1 -1
- package/stl-docs/index.ts +58 -23
- package/stl-docs/loadStlDocsConfig.ts +17 -2
- package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +64 -0
- package/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts +33 -0
- package/stl-docs/proseMarkdown/toMarkdown.ts +158 -0
- package/styles/page.css +31 -4
- package/virtual-module.d.ts +3 -2
- package/plugin/globalJs/ai-dropdown.ts +0 -57
- package/stl-docs/components/APIReferenceAIDropdown.tsx +0 -58
- package/stl-docs/components/content-panel/ProseAIDropdown.tsx +0 -55
- /package/stl-docs/components/{content-panel/ContentBreadcrumbs.tsx → ContentBreadcrumbs.tsx} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @stainless-api/docs
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.17
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a925bb4: fix markdown icon color in dark mode
|
|
8
|
+
|
|
9
|
+
## 0.1.0-beta.16
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 0618d05: Markdown rendering of prose pages, ai context menu, auth via cli, minor fixes
|
|
14
|
+
- Updated dependencies [0618d05]
|
|
15
|
+
- @stainless-api/docs-ui@0.1.0-beta.13
|
|
16
|
+
- @stainless-api/ui-primitives@0.1.0-beta.14
|
|
17
|
+
|
|
3
18
|
## 0.1.0-beta.15
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stainless-api/docs",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.17",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -9,23 +9,9 @@
|
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./stl-docs/index.ts",
|
|
11
11
|
"./plugin": "./plugin/index.ts",
|
|
12
|
-
"./OverviewRoute": "./plugin/routes/Overview.astro",
|
|
13
|
-
"./DocsRoute": "./plugin/routes/Docs.astro",
|
|
14
|
-
"./DocsStaticRoute": "./plugin/routes/DocsStatic.astro",
|
|
15
|
-
"./MarkdownRoute": "./plugin/routes/markdown.ts",
|
|
16
|
-
"./Search": "./plugin/components/search/Search.astro",
|
|
17
|
-
"./replaceSidebarPlaceholderMiddleware": "./plugin/replaceSidebarPlaceholderMiddleware.ts",
|
|
18
12
|
"./plugin/middleware": "./plugin/middlewareBuilder/stlStarlightMiddleware.ts",
|
|
19
13
|
"./plugin/MiddlewareTypes": "./plugin/middlewareBuilder/stainlessMiddleware.d.ts",
|
|
20
|
-
"./Header": "./stl-docs/components/Header.astro",
|
|
21
|
-
"./ThemeSelect": "./stl-docs/components/ThemeSelect.astro",
|
|
22
|
-
"./BaseSidebar": "./stl-docs/components/sidebars/BaseSidebar.astro",
|
|
23
|
-
"./SDKSelectSidebar": "./stl-docs/components/sidebars/SDKSelectSidebar.astro",
|
|
24
|
-
"./ContentPanel": "./stl-docs/components/content-panel/ContentPanel.astro",
|
|
25
|
-
"./TableOfContents": "./stl-docs/components/TableOfContents.astro",
|
|
26
|
-
"./tabsMiddleware": "./stl-docs/tabsMiddleware.ts",
|
|
27
14
|
"./stainless-docs/mintlify-compat": "./stl-docs/components/mintlify-compat/index.ts",
|
|
28
|
-
"./theme": "./theme.css",
|
|
29
15
|
"./mintlify-compat.css": "./styles/mintlify-compat.css",
|
|
30
16
|
"./font-imports": "./styles/fonts.css",
|
|
31
17
|
"./components": "./stl-docs/components/index.ts"
|
|
@@ -57,8 +43,14 @@
|
|
|
57
43
|
"shiki": "^3.9.2",
|
|
58
44
|
"web-worker": "^1.5.0",
|
|
59
45
|
"yaml": "^2.8.0",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
46
|
+
"node-html-parser": "^7.0.1",
|
|
47
|
+
"rehype-parse": "^9.0.1",
|
|
48
|
+
"rehype-remark": "^10.0.1",
|
|
49
|
+
"remark-gfm": "^4.0.1",
|
|
50
|
+
"remark-stringify": "^11.0.0",
|
|
51
|
+
"unified": "^11.0.5",
|
|
52
|
+
"@stainless-api/docs-ui": "0.1.0-beta.13",
|
|
53
|
+
"@stainless-api/ui-primitives": "0.1.0-beta.14"
|
|
62
54
|
},
|
|
63
55
|
"devDependencies": {
|
|
64
56
|
"@markdoc/markdoc": "^0.5.2",
|
|
@@ -5,6 +5,7 @@ import type * as SDKJSON from '~/lib/json-spec-v2/types';
|
|
|
5
5
|
import { Languages } from '@stainless-api/docs-ui/src/routing';
|
|
6
6
|
import { buildIndex } from '@stainless-api/docs-ui/src/search/providers/algolia';
|
|
7
7
|
import type { VersionUserConfig } from './loadPluginConfig';
|
|
8
|
+
import type { AstroIntegrationLogger } from 'astro';
|
|
8
9
|
|
|
9
10
|
const markdocConfig = {
|
|
10
11
|
nodes: {
|
|
@@ -20,7 +21,31 @@ function renderMarkdown(content?: string) {
|
|
|
20
21
|
return Markdoc.renderers.html(transformed);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export async function buildAlgoliaIndex({
|
|
24
|
+
export async function buildAlgoliaIndex({
|
|
25
|
+
version,
|
|
26
|
+
apiKey,
|
|
27
|
+
logger,
|
|
28
|
+
}: {
|
|
29
|
+
version: VersionUserConfig;
|
|
30
|
+
apiKey: string;
|
|
31
|
+
logger?: AstroIntegrationLogger;
|
|
32
|
+
}) {
|
|
33
|
+
function warnLog(message: string) {
|
|
34
|
+
if (logger) {
|
|
35
|
+
logger.warn(message);
|
|
36
|
+
} else {
|
|
37
|
+
console.warn(message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function infoLog(message: string) {
|
|
42
|
+
if (logger) {
|
|
43
|
+
logger.info(message);
|
|
44
|
+
} else {
|
|
45
|
+
console.log(message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
24
49
|
const client = new Stainless({ apiKey });
|
|
25
50
|
const configs = await client.projects.configs.retrieve({
|
|
26
51
|
project: version.stainlessProject,
|
|
@@ -64,9 +89,9 @@ export async function buildAlgoliaIndex({ version, apiKey }: { version: VersionU
|
|
|
64
89
|
!indexName && 'PUBLIC_ALGOLIA_INDEX',
|
|
65
90
|
!algoliaWriteKey && 'PRIVATE_ALGOLIA_WRITE_KEY',
|
|
66
91
|
].filter(Boolean);
|
|
67
|
-
|
|
92
|
+
warnLog(`Skipping Algolia indexing due to missing environment variables: ${missing.join(', ')}`);
|
|
68
93
|
return;
|
|
69
94
|
}
|
|
70
95
|
await buildIndex(appId, indexName, algoliaWriteKey, sdkJson, renderMarkdown);
|
|
71
|
-
|
|
96
|
+
infoLog('Indexing complete.');
|
|
72
97
|
}
|
package/plugin/cms/server.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import type { AstroIntegrationLogger } from 'astro';
|
|
1
2
|
import { createServer, IncomingMessage } from 'http';
|
|
2
3
|
import { readFile } from 'fs/promises';
|
|
3
4
|
|
|
4
5
|
import type * as SDKJSON from '~/lib/json-spec-v2/types';
|
|
5
6
|
import { createSDKJSON, parseInputs, transformOAS } from './worker';
|
|
6
7
|
import { Languages, parseRoute, type DocsLanguage } from '@stainless-api/docs-ui/src/routing';
|
|
7
|
-
import Stainless from '@stainless-api/sdk';
|
|
8
|
+
import Stainless, { APIError } from '@stainless-api/sdk';
|
|
8
9
|
|
|
9
10
|
import {
|
|
10
11
|
toStarlightSidebar,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
SidebarConfigItemsBuilder,
|
|
13
14
|
} from './sidebar-builder';
|
|
14
15
|
import type { VersionUserConfig } from '../loadPluginConfig';
|
|
16
|
+
import { bold } from '../../shared/terminalUtils';
|
|
15
17
|
|
|
16
18
|
export type InputFilePaths = {
|
|
17
19
|
oasPath?: string;
|
|
@@ -24,76 +26,105 @@ async function versionInfo(project: string, apiKey: string) {
|
|
|
24
26
|
});
|
|
25
27
|
|
|
26
28
|
const content = await data.text();
|
|
27
|
-
return JSON.parse(content)
|
|
29
|
+
return JSON.parse(content) as Record<DocsLanguage, string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function redactApiKey(apiKey: string) {
|
|
33
|
+
return apiKey
|
|
34
|
+
.split('')
|
|
35
|
+
.map((char, index) => (index < 10 ? char : '*'))
|
|
36
|
+
.join('');
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
async function loadSpec({
|
|
31
40
|
apiKey,
|
|
32
41
|
devPaths,
|
|
33
42
|
version,
|
|
43
|
+
logger,
|
|
34
44
|
}: {
|
|
35
45
|
apiKey: string;
|
|
36
46
|
devPaths: InputFilePaths;
|
|
37
47
|
version: VersionUserConfig;
|
|
48
|
+
logger: AstroIntegrationLogger;
|
|
38
49
|
}) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
async function unsafeLoad() {
|
|
51
|
+
let oasStr: string;
|
|
52
|
+
let configStr: string;
|
|
53
|
+
let versions: Record<DocsLanguage, string> | undefined;
|
|
54
|
+
|
|
55
|
+
if (devPaths.oasPath && devPaths.configPath) {
|
|
56
|
+
[oasStr, configStr] = await Promise.all([
|
|
57
|
+
readFile(devPaths.oasPath, 'utf-8'),
|
|
58
|
+
readFile(devPaths.configPath, 'utf-8'),
|
|
59
|
+
]);
|
|
60
|
+
} else {
|
|
61
|
+
const client = new Stainless({ apiKey });
|
|
62
|
+
const configs = await client.projects.configs.retrieve({
|
|
63
|
+
project: version.stainlessProject,
|
|
64
|
+
branch: version.branch,
|
|
65
|
+
include: 'openapi',
|
|
66
|
+
});
|
|
55
67
|
|
|
56
|
-
|
|
68
|
+
versions = await versionInfo(version.stainlessProject, apiKey);
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
const configYML = Object.values(configs)[0] as { content: any };
|
|
71
|
+
const oasJson = Object.values(configs)[1] as { content: any };
|
|
72
|
+
oasStr = oasJson['content'];
|
|
73
|
+
configStr = configYML['content'];
|
|
74
|
+
}
|
|
63
75
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
const { oas, config } = await parseInputs({
|
|
77
|
+
oas: oasStr,
|
|
78
|
+
config: configStr,
|
|
79
|
+
});
|
|
68
80
|
|
|
69
|
-
|
|
81
|
+
const transformedOAS = await transformOAS({ oas, config });
|
|
70
82
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
const languages =
|
|
84
|
+
config.docs?.languages ??
|
|
85
|
+
(Object.entries(config.targets)
|
|
86
|
+
// @ts-expect-error we don't have the actual Stainless config type here
|
|
87
|
+
.filter(([name, target]) => Languages.includes(name) && !target.skip)
|
|
88
|
+
.map(([name]) => name) as SDKJSON.SpecLanguage[]);
|
|
77
89
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
const sdkJson = await createSDKJSON({
|
|
91
|
+
oas: transformedOAS,
|
|
92
|
+
config,
|
|
93
|
+
languages,
|
|
94
|
+
});
|
|
83
95
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
if (versions) {
|
|
97
|
+
for (const [lang, version] of Object.entries(versions)) {
|
|
98
|
+
const meta = sdkJson.metadata[lang as DocsLanguage];
|
|
99
|
+
if (meta?.version) meta.version = version;
|
|
100
|
+
}
|
|
88
101
|
}
|
|
102
|
+
|
|
103
|
+
const id = crypto.randomUUID();
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
data: sdkJson,
|
|
107
|
+
id,
|
|
108
|
+
};
|
|
89
109
|
}
|
|
90
110
|
|
|
91
|
-
|
|
111
|
+
try {
|
|
112
|
+
const result = await unsafeLoad();
|
|
113
|
+
return result;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
logger.error(bold('Failed to fetch API reference information from Stainless:'));
|
|
116
|
+
if (error instanceof APIError && error.status >= 400 && error.status < 500) {
|
|
117
|
+
logger.error(`Requested project slug: "${version.stainlessProject}"`);
|
|
118
|
+
logger.error(`API key: "${redactApiKey(apiKey)}"`);
|
|
119
|
+
logger.error(
|
|
120
|
+
`This error can usually be corrected by re-authenticating with the Stainless. Use the CLI (stl auth login) or verify that the Stainless API key you're using can access the project mentioned above.`,
|
|
121
|
+
);
|
|
122
|
+
} else {
|
|
123
|
+
logger.error(error instanceof Error ? error.message : 'Unknown error');
|
|
124
|
+
}
|
|
92
125
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
id,
|
|
96
|
-
};
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
97
128
|
}
|
|
98
129
|
|
|
99
130
|
class Spec {
|
|
@@ -107,6 +138,7 @@ class Spec {
|
|
|
107
138
|
apiKey: this.apiKey,
|
|
108
139
|
devPaths: this.devPaths,
|
|
109
140
|
version: this.version,
|
|
141
|
+
logger: this.logger,
|
|
110
142
|
});
|
|
111
143
|
}
|
|
112
144
|
|
|
@@ -128,15 +160,22 @@ class Spec {
|
|
|
128
160
|
return spec;
|
|
129
161
|
}
|
|
130
162
|
|
|
131
|
-
constructor(
|
|
163
|
+
constructor(
|
|
164
|
+
apiKey: string,
|
|
165
|
+
version: VersionUserConfig,
|
|
166
|
+
devPaths: InputFilePaths,
|
|
167
|
+
private logger: AstroIntegrationLogger,
|
|
168
|
+
) {
|
|
132
169
|
this.specPromise = loadSpec({
|
|
133
170
|
apiKey,
|
|
134
171
|
devPaths,
|
|
135
172
|
version,
|
|
173
|
+
logger,
|
|
136
174
|
});
|
|
137
175
|
this.devPaths = devPaths;
|
|
138
176
|
this.apiKey = apiKey;
|
|
139
177
|
this.version = version;
|
|
178
|
+
this.logger = logger;
|
|
140
179
|
}
|
|
141
180
|
}
|
|
142
181
|
|
|
@@ -166,14 +205,16 @@ export function startDevServer({
|
|
|
166
205
|
devPaths,
|
|
167
206
|
apiKey,
|
|
168
207
|
getGeneratedSidebarConfig,
|
|
208
|
+
logger,
|
|
169
209
|
}: {
|
|
170
210
|
port: number;
|
|
171
211
|
version: VersionUserConfig;
|
|
172
212
|
devPaths: InputFilePaths;
|
|
173
213
|
apiKey: string;
|
|
174
214
|
getGeneratedSidebarConfig: (id: number) => GeneratedSidebarConfig | null;
|
|
215
|
+
logger: AstroIntegrationLogger;
|
|
175
216
|
}) {
|
|
176
|
-
const spec = new Spec(apiKey, version, devPaths);
|
|
217
|
+
const spec = new Spec(apiKey, version, devPaths, logger);
|
|
177
218
|
|
|
178
219
|
const server = createServer(async (req, res) => {
|
|
179
220
|
// Add CORS headers
|
|
@@ -250,7 +291,7 @@ export function startDevServer({
|
|
|
250
291
|
});
|
|
251
292
|
|
|
252
293
|
server.listen(port, () => {
|
|
253
|
-
|
|
294
|
+
logger.debug(`Stainless spec server is running on port: ${port}`);
|
|
254
295
|
});
|
|
255
296
|
|
|
256
297
|
return {
|
|
@@ -266,3 +307,5 @@ export function startDevServer({
|
|
|
266
307
|
},
|
|
267
308
|
};
|
|
268
309
|
}
|
|
310
|
+
|
|
311
|
+
export type DevSpecServer = Awaited<ReturnType<typeof startDevServer>>;
|
|
@@ -208,13 +208,6 @@ export class SidebarConfigItemsBuilder {
|
|
|
208
208
|
};
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
private sortByLabel<T extends UserSidebarConfigItem>(items: T[]) {
|
|
212
|
-
// sorts in place
|
|
213
|
-
items.sort((a, b) => {
|
|
214
|
-
return a.label.localeCompare(b.label);
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
211
|
private generateResourceGroup(resource: SDKJSON.Resource, collapsed: boolean): ReferenceSidebarGroup {
|
|
219
212
|
const entries: ReferenceSidebarConfigItem[] = [];
|
|
220
213
|
if (!this.options?.excludeResourceOverviewPages) {
|
|
@@ -228,7 +221,6 @@ export class SidebarConfigItemsBuilder {
|
|
|
228
221
|
methodPages.push(this.toMethodPage(m, langDecl));
|
|
229
222
|
}
|
|
230
223
|
}
|
|
231
|
-
this.sortByLabel(methodPages);
|
|
232
224
|
entries.push(...methodPages);
|
|
233
225
|
|
|
234
226
|
const subresources = Object.values(resource.subresources ?? {});
|
|
@@ -238,7 +230,6 @@ export class SidebarConfigItemsBuilder {
|
|
|
238
230
|
subresourceGroups.push(this.generateResourceGroup(sub, true));
|
|
239
231
|
}
|
|
240
232
|
}
|
|
241
|
-
this.sortByLabel(subresourceGroups);
|
|
242
233
|
entries.push(...subresourceGroups);
|
|
243
234
|
|
|
244
235
|
return {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { initDropdownButton } from '@stainless-api/ui-primitives/scripts';
|
|
2
|
+
import { getPageLoadEvent } from '../helpers/getPageLoadEvent';
|
|
3
|
+
|
|
4
|
+
export type DropdownIcon = 'markdown' | 'copy' | 'claude' | 'chatgpt';
|
|
5
|
+
|
|
6
|
+
interface DropdownOptionInputProps {
|
|
7
|
+
onClick: () => void;
|
|
8
|
+
icon: DropdownIcon;
|
|
9
|
+
primaryAction?: boolean;
|
|
10
|
+
label: string[] | string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getMarkdownUrl(type: 'relative' | 'absolute') {
|
|
14
|
+
const currentUrl = new URL(window.location.href);
|
|
15
|
+
const hasTrailingSlash = currentUrl.pathname.endsWith('/');
|
|
16
|
+
|
|
17
|
+
const markdownUrl = [
|
|
18
|
+
type === 'absolute' ? currentUrl.origin : '',
|
|
19
|
+
currentUrl.pathname,
|
|
20
|
+
hasTrailingSlash ? '' : '/',
|
|
21
|
+
'index.md',
|
|
22
|
+
].join('');
|
|
23
|
+
|
|
24
|
+
return markdownUrl;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function openInLLM(serviceUrl: string) {
|
|
28
|
+
const mdUrl = getMarkdownUrl('absolute');
|
|
29
|
+
const prompt = encodeURIComponent(
|
|
30
|
+
`Load the contents of ${mdUrl} into this chat's context so we can discuss it.`,
|
|
31
|
+
);
|
|
32
|
+
window.open(`${serviceUrl}${prompt}`, '_blank');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2d array of dropdown options
|
|
36
|
+
// each sub-array is a group, separated by a horizontal rule in the UI
|
|
37
|
+
const aiDropdownOptions: DropdownOptionInputProps[][] = [
|
|
38
|
+
[
|
|
39
|
+
{
|
|
40
|
+
label: 'View as Markdown',
|
|
41
|
+
onClick: () => {
|
|
42
|
+
window.open(getMarkdownUrl('absolute'), '_blank');
|
|
43
|
+
},
|
|
44
|
+
icon: 'markdown',
|
|
45
|
+
primaryAction: true,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: 'Copy Markdown',
|
|
49
|
+
onClick: () => {
|
|
50
|
+
// Source: https://wolfgangrittner.dev/how-to-use-clipboard-api-in-firefox/
|
|
51
|
+
const markdownUrl = getMarkdownUrl('relative');
|
|
52
|
+
// ClipboardItem doesn't exist in every browser
|
|
53
|
+
// eslint-disable-next-line no-constant-binary-expression
|
|
54
|
+
if (typeof ClipboardItem && navigator.clipboard.write) {
|
|
55
|
+
// NOTE: Safari locks down the clipboard API to only work when triggered
|
|
56
|
+
// by a direct user interaction. You can't use it async in a promise.
|
|
57
|
+
// But! You can wrap the promise in a ClipboardItem, and give that to
|
|
58
|
+
// the clipboard API.
|
|
59
|
+
// Found this on https://developer.apple.com/forums/thread/691873
|
|
60
|
+
const text = new ClipboardItem({
|
|
61
|
+
'text/plain': fetch(markdownUrl)
|
|
62
|
+
.then((response) => response.text())
|
|
63
|
+
.then((text) => new Blob([text], { type: 'text/plain' })),
|
|
64
|
+
});
|
|
65
|
+
navigator.clipboard.write([text]);
|
|
66
|
+
} else {
|
|
67
|
+
// NOTE: Firefox has support for ClipboardItem and navigator.clipboard.write,
|
|
68
|
+
// but those are behind `dom.events.asyncClipboard.clipboardItem` preference.
|
|
69
|
+
// Good news is that other than Safari, Firefox does not care about
|
|
70
|
+
// Clipboard API being used async in a Promise.
|
|
71
|
+
fetch(markdownUrl)
|
|
72
|
+
.then((response) => response.text())
|
|
73
|
+
.then((text) => navigator.clipboard.writeText(text));
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
icon: 'copy',
|
|
77
|
+
primaryAction: false,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
[
|
|
81
|
+
{
|
|
82
|
+
label: ['Open in ', 'Claude'],
|
|
83
|
+
onClick: () => {
|
|
84
|
+
openInLLM('https://claude.ai/new?q=');
|
|
85
|
+
},
|
|
86
|
+
icon: 'claude',
|
|
87
|
+
primaryAction: false,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
label: ['Open in ', 'ChatGPT'],
|
|
91
|
+
onClick: () => {
|
|
92
|
+
openInLLM('https://chatgpt.com/?hints=search&prompt=');
|
|
93
|
+
},
|
|
94
|
+
icon: 'chatgpt',
|
|
95
|
+
primaryAction: false,
|
|
96
|
+
},
|
|
97
|
+
// TODO: Add Cursor support
|
|
98
|
+
// {
|
|
99
|
+
// label: ['Open in ', 'Cursor'],
|
|
100
|
+
// onClick: () => {
|
|
101
|
+
// openInLLM('https://www.cursor.so/?prompt=');
|
|
102
|
+
// },
|
|
103
|
+
// icon: 'cursor',
|
|
104
|
+
// primaryAction: false,
|
|
105
|
+
// }
|
|
106
|
+
],
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
function renderGroup(group: DropdownOptionInputProps[]) {
|
|
110
|
+
return group.map((option) => {
|
|
111
|
+
const label = typeof option.label === 'string' ? [option.label] : option.label;
|
|
112
|
+
return {
|
|
113
|
+
...option,
|
|
114
|
+
label: label,
|
|
115
|
+
primaryAction: option.primaryAction ?? false,
|
|
116
|
+
id: label.join('').toLowerCase().replace(/ /g, '-'),
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function getAIDropdownOptions() {
|
|
122
|
+
const renderedOptions = aiDropdownOptions.map((group, index) => {
|
|
123
|
+
return {
|
|
124
|
+
options: renderGroup(group),
|
|
125
|
+
isLast: index === aiDropdownOptions.length - 1,
|
|
126
|
+
reactKey: index,
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const allOptions = renderedOptions.flatMap((group) => group.options);
|
|
131
|
+
const primaryAction = allOptions.find((o) => o.primaryAction) ?? allOptions[0]!;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
primaryAction,
|
|
135
|
+
groups: renderedOptions,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function wireAIDropdown() {
|
|
140
|
+
const { primaryAction, groups } = getAIDropdownOptions();
|
|
141
|
+
function triggerOption(id: string) {
|
|
142
|
+
const option = groups.flatMap((group) => group.options).find((option) => option.id === id);
|
|
143
|
+
if (!option) return;
|
|
144
|
+
option.onClick();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
document.addEventListener(getPageLoadEvent(), () => {
|
|
148
|
+
const dropdowns = document.querySelectorAll('[data-dropdown-id]');
|
|
149
|
+
dropdowns.forEach((dropdown) => {
|
|
150
|
+
initDropdownButton({
|
|
151
|
+
dropdown: dropdown,
|
|
152
|
+
onSelect: (value) => {
|
|
153
|
+
triggerOption(value);
|
|
154
|
+
},
|
|
155
|
+
onPrimaryAction: () => {
|
|
156
|
+
triggerOption(primaryAction.id);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
@@ -5,8 +5,6 @@ import { navigate } from 'astro:transitions/client';
|
|
|
5
5
|
import { getPageLoadEvent } from '../helpers/getPageLoadEvent.ts';
|
|
6
6
|
|
|
7
7
|
import { initDropdown } from '@stainless-api/docs-ui/src/components/scripts/dropdown';
|
|
8
|
-
import { initDropdownButton } from '@stainless-api/ui-primitives/scripts';
|
|
9
|
-
import { copyCurrentPageAsMarkdown, onSelectAIOption } from './ai-dropdown.ts';
|
|
10
8
|
|
|
11
9
|
history.scrollRestoration = 'auto';
|
|
12
10
|
|
|
@@ -36,26 +34,6 @@ document.addEventListener(getPageLoadEvent(), () => {
|
|
|
36
34
|
if (path) setTimeout(() => scrollToPath(path.slice(1)), 10);
|
|
37
35
|
});
|
|
38
36
|
|
|
39
|
-
document.addEventListener(getPageLoadEvent(), () => {
|
|
40
|
-
initDropdownButton({
|
|
41
|
-
dropdownId: 'ai-dropdown-button',
|
|
42
|
-
onSelect: onSelectAIOption,
|
|
43
|
-
onPrimaryAction: (el) => {
|
|
44
|
-
copyCurrentPageAsMarkdown();
|
|
45
|
-
const innerText = el.querySelector('[data-part="primary-action-text"]');
|
|
46
|
-
if (!innerText) return;
|
|
47
|
-
|
|
48
|
-
const originalInnerHtml = innerText.innerHTML;
|
|
49
|
-
innerText.innerHTML = 'Copied!';
|
|
50
|
-
el.classList.add('disabled');
|
|
51
|
-
setTimeout(() => {
|
|
52
|
-
innerText.innerHTML = originalInnerHtml;
|
|
53
|
-
el.classList.remove('disabled');
|
|
54
|
-
}, 1000);
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
37
|
document.addEventListener('click', (event) => {
|
|
60
38
|
const toggle = (event.target as HTMLElement).closest(
|
|
61
39
|
'[data-stldocs-property-toggle-expanded] > .stldocs-expand-toggle-content',
|