@stainless-api/docs-ui 0.1.0-beta.2 → 0.1.0-beta.20
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/dist/index.js +1312 -1871
- package/dist/mcp.cjs +983441 -0
- package/dist/routing.js +4 -4
- package/dist/styles/main.css +743 -747
- package/dist/styles/primitives.css +444 -426
- package/dist/styles/resets.css +33 -41
- package/dist/styles/search.css +265 -248
- package/dist/styles/sidebar.css +58 -60
- package/dist/styles/snippets.css +86 -88
- package/dist/styles/variables.css +85 -89
- package/package.json +19 -10
- package/src/components/breadcrumbs.tsx +1 -1
- package/src/components/chat.tsx +18 -15
- package/src/components/method.tsx +12 -11
- package/src/components/overview.tsx +32 -19
- package/src/components/primitives.tsx +36 -19
- package/src/components/properties.tsx +4 -2
- package/src/components/scripts/dropdown.ts +1 -1
- package/src/components/sdk.tsx +28 -22
- package/src/components/sidebar.tsx +3 -3
- package/src/components/snippets.tsx +29 -11
- package/src/contexts/component-generics.tsx +10 -15
- package/src/contexts/docs.tsx +15 -4
- package/src/contexts/index.tsx +8 -5
- package/src/contexts/markdown.tsx +7 -6
- package/src/contexts/search.tsx +4 -5
- package/src/hooks/use-strict-context.tsx +16 -0
- package/src/languages/go.tsx +3 -3
- package/src/languages/http.tsx +31 -23
- package/src/languages/index.ts +7 -7
- package/src/languages/java.tsx +4 -4
- package/src/languages/python.tsx +12 -9
- package/src/languages/ruby.tsx +20 -13
- package/src/languages/typescript.tsx +18 -12
- package/src/markdown/index.ts +17 -12
- package/src/markdown/utils.ts +6 -3
- package/src/routing.ts +9 -9
- package/src/search/form.tsx +11 -8
- package/src/search/indexer.ts +17 -15
- package/src/search/mcp.ts +108 -16
- package/src/search/printer.tsx +1 -1
- package/src/search/providers/algolia.ts +5 -5
- package/src/search/providers/fuse.ts +4 -4
- package/src/search/providers/pagefind.ts +1 -1
- package/src/search/providers/walker.ts +5 -3
- package/src/search/results.tsx +9 -7
- package/src/search/types.ts +2 -2
- package/src/style.ts +1 -1
- package/src/styles/main.css +743 -747
- package/src/styles/primitives.css +444 -426
- package/src/styles/resets.css +33 -41
- package/src/styles/search.css +265 -248
- package/src/styles/sidebar.css +58 -60
- package/src/styles/snippets.css +86 -88
- package/src/styles/variables.css +85 -89
- package/src/utils.ts +14 -15
- package/dist/mcp.js +0 -16003
package/src/markdown/utils.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Parser } from 'htmlparser2';
|
|
2
2
|
import type { DocsLanguage } from '../routing';
|
|
3
|
-
import type * as SDKJSON from '
|
|
3
|
+
import type * as SDKJSON from '@stainless/sdk-json';
|
|
4
4
|
import type { TransformRequestSnippetFn } from '../components/sdk';
|
|
5
5
|
|
|
6
6
|
export type EnvironmentType = {
|
|
@@ -19,7 +19,10 @@ export function getDecl(env: EnvironmentType, path: string) {
|
|
|
19
19
|
const decl = env.spec?.decls?.[env.language]?.[path];
|
|
20
20
|
|
|
21
21
|
if (decl?.kind?.endsWith('Reference')) {
|
|
22
|
-
const refId =
|
|
22
|
+
const refId =
|
|
23
|
+
'type' in decl && typeof decl['type'] === 'object' && '$ref' in decl['type']
|
|
24
|
+
? decl['type']['$ref']
|
|
25
|
+
: null;
|
|
23
26
|
if (refId === path) return decl;
|
|
24
27
|
if (refId) return getDecl(env, refId);
|
|
25
28
|
}
|
|
@@ -28,7 +31,7 @@ export function getDecl(env: EnvironmentType, path: string) {
|
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export function getSnippet(env: EnvironmentType, path: string) {
|
|
31
|
-
let snippet = env.spec?.snippets?.[`${env.language}.default`]?.[path];
|
|
34
|
+
let snippet = env.spec?.snippets?.[`${env.language}.default` as SDKJSON.SnippetLanguage]?.[path];
|
|
32
35
|
if (typeof snippet === 'string' && env.transforms?.transformRequestSnippet) {
|
|
33
36
|
snippet = env.transforms.transformRequestSnippet({ snippet, language: env.language });
|
|
34
37
|
}
|
package/src/routing.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type * as SDKJSON from '
|
|
1
|
+
import type * as SDKJSON from '@stainless/sdk-json';
|
|
2
2
|
|
|
3
3
|
export const Languages = [
|
|
4
4
|
'http',
|
|
@@ -56,10 +56,10 @@ export type ParsedStainlessPath = ReturnType<typeof parseStainlessPath>;
|
|
|
56
56
|
export function parseStainlessPath(stainlessPath: string) {
|
|
57
57
|
const match = stainlessPath.match(StainlessPathPattern);
|
|
58
58
|
|
|
59
|
-
if (!match) return null;
|
|
59
|
+
if (!match?.groups) return null;
|
|
60
60
|
|
|
61
61
|
return {
|
|
62
|
-
resource: match.groups.resource
|
|
62
|
+
resource: match.groups.resource?.split('.') ?? null,
|
|
63
63
|
method: match.groups.method ?? null,
|
|
64
64
|
model: match.groups.model ?? null,
|
|
65
65
|
routable: match.groups.model ? match[1] : match[0],
|
|
@@ -72,7 +72,7 @@ export function trimStainlessPath(stainlessPath: string) {
|
|
|
72
72
|
|
|
73
73
|
export function getResource(stainlessPath: string) {
|
|
74
74
|
const parsed = parseStainlessPath(stainlessPath);
|
|
75
|
-
return parsed?.resource[0];
|
|
75
|
+
return parsed?.resource?.[0];
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export function parseRoute(
|
|
@@ -119,7 +119,7 @@ export function generateRoute(basePath: string, language: string, stainlessPath:
|
|
|
119
119
|
const path = [basePath.endsWith('/') ? basePath.slice(0, -1) : basePath];
|
|
120
120
|
if (language && language !== DefaultLanguage) path.push(language);
|
|
121
121
|
|
|
122
|
-
const resources = parsedPath.resource
|
|
122
|
+
const resources = parsedPath.resource!.flatMap((name, index) => [
|
|
123
123
|
index > 0 ? 'subresources' : 'resources',
|
|
124
124
|
name,
|
|
125
125
|
]);
|
|
@@ -130,7 +130,7 @@ export function generateRoute(basePath: string, language: string, stainlessPath:
|
|
|
130
130
|
|
|
131
131
|
if (parsedPath.method) path.push('methods', parsedPath.method);
|
|
132
132
|
|
|
133
|
-
return stainlessPath.length > parsedPath.routable
|
|
133
|
+
return stainlessPath.length > parsedPath.routable!.length
|
|
134
134
|
? `${path.join('/')}#${encodeURIComponent(stainlessPath)}`
|
|
135
135
|
: path.join('/');
|
|
136
136
|
}
|
|
@@ -203,7 +203,7 @@ export function generateRouteList({
|
|
|
203
203
|
|
|
204
204
|
type ResourceOrMethod = SDKJSON.Resource | SDKJSON.Method;
|
|
205
205
|
|
|
206
|
-
export function findNavigationPath(items: ResourceOrMethod[], target: string) {
|
|
206
|
+
export function findNavigationPath(items: ResourceOrMethod[], target: string): string[] | undefined {
|
|
207
207
|
for (const item of Object.values(items)) {
|
|
208
208
|
if (item.stainlessPath === target) return [item.stainlessPath];
|
|
209
209
|
if (item.kind === 'http_method') continue;
|
|
@@ -217,9 +217,9 @@ export function findNavigationPath(items: ResourceOrMethod[], target: string) {
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
export function expandToElement(el: HTMLElement) {
|
|
220
|
+
export function expandToElement(el: HTMLElement | null) {
|
|
221
221
|
while (el) {
|
|
222
|
-
if (el
|
|
222
|
+
if (el instanceof HTMLDetailsElement) el.open = true;
|
|
223
223
|
el = el.parentElement;
|
|
224
224
|
}
|
|
225
225
|
}
|
package/src/search/form.tsx
CHANGED
|
@@ -14,7 +14,7 @@ export function SearchForm() {
|
|
|
14
14
|
const language = useLanguage();
|
|
15
15
|
const { onSelect, pageFind } = useSearchContext();
|
|
16
16
|
|
|
17
|
-
const [results, setResults] = React.useState<ResultData>(null);
|
|
17
|
+
const [results, setResults] = React.useState<ResultData>(null!);
|
|
18
18
|
const [filterKind, setFilterKind] = React.useState<QueryKindsType>('all');
|
|
19
19
|
const [searchQuery, setSearchQuery] = React.useState<string>('');
|
|
20
20
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
@@ -29,11 +29,11 @@ export function SearchForm() {
|
|
|
29
29
|
]);
|
|
30
30
|
|
|
31
31
|
setResults({
|
|
32
|
-
items: filterKind === 'guide' ? guideResults : [...guideResults, ...apiResults
|
|
32
|
+
items: filterKind === 'guide' ? guideResults : [...guideResults, ...(apiResults?.hits ?? [])],
|
|
33
33
|
counts: {
|
|
34
|
-
...apiResults
|
|
34
|
+
...apiResults?.facets?.['kind'],
|
|
35
35
|
guide: guideResults.length,
|
|
36
|
-
all: apiResults
|
|
36
|
+
all: apiResults?.nbHits,
|
|
37
37
|
},
|
|
38
38
|
});
|
|
39
39
|
}
|
|
@@ -43,6 +43,7 @@ export function SearchForm() {
|
|
|
43
43
|
inputRef?.current?.focus();
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
47
|
React.useEffect(() => void performSearch(), [searchQuery, filterKind, language]);
|
|
47
48
|
|
|
48
49
|
return (
|
|
@@ -72,7 +73,9 @@ export function SearchForm() {
|
|
|
72
73
|
itemDelegate={(item) =>
|
|
73
74
|
'kind' in item ? <SearchResult result={item} /> : <GuideResult result={item} />
|
|
74
75
|
}
|
|
75
|
-
onSelectListItem={(item) =>
|
|
76
|
+
onSelectListItem={(item) =>
|
|
77
|
+
onSelect?.((item as any)['data']?.['url'] ?? (item as any)['stainlessPath'])
|
|
78
|
+
}
|
|
76
79
|
/>
|
|
77
80
|
</div>
|
|
78
81
|
);
|
|
@@ -81,7 +84,7 @@ export function SearchForm() {
|
|
|
81
84
|
export type SearchFilterProps = {
|
|
82
85
|
results: ResultData;
|
|
83
86
|
filterKind: QueryKindsType;
|
|
84
|
-
onChange: (filterKind
|
|
87
|
+
onChange: (filterKind: QueryKindsType) => void;
|
|
85
88
|
};
|
|
86
89
|
|
|
87
90
|
export function SearchFilter({ results, filterKind, onChange }: SearchFilterProps) {
|
|
@@ -97,7 +100,7 @@ export function SearchFilter({ results, filterKind, onChange }: SearchFilterProp
|
|
|
97
100
|
size: 16,
|
|
98
101
|
className: style.Icon,
|
|
99
102
|
})}
|
|
100
|
-
{QueryKindDisplay[kind].name}
|
|
103
|
+
<span className={style.SearchFilterLabel}>{QueryKindDisplay[kind].name}</span>
|
|
101
104
|
<span className={style.SearchFilterCount}>{results?.counts?.[kind] ?? 0}</span>
|
|
102
105
|
</Docs.ToggleButton>
|
|
103
106
|
))}
|
|
@@ -111,7 +114,7 @@ export type SearchModalProps = {
|
|
|
111
114
|
};
|
|
112
115
|
|
|
113
116
|
export function SearchModal({ id, open: isOpen }: SearchModalProps) {
|
|
114
|
-
const [open, setOpen] = React.useState<boolean>(isOpen);
|
|
117
|
+
const [open, setOpen] = React.useState<boolean>(isOpen ?? false);
|
|
115
118
|
|
|
116
119
|
return (
|
|
117
120
|
<div
|
package/src/search/indexer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { generateRoute, Languages, parseStainlessPath, walkTree } from '../routing';
|
|
2
2
|
import * as printer from './printer';
|
|
3
|
-
import type * as SDKJSON from '
|
|
3
|
+
import type * as SDKJSON from '@stainless/sdk-json';
|
|
4
4
|
import type { IndexEntry } from './types';
|
|
5
5
|
import { renderMarkdown } from '../markdown';
|
|
6
6
|
|
|
@@ -93,8 +93,8 @@ export function* generateChatIndex(spec: SDKJSON.Spec) {
|
|
|
93
93
|
summary,
|
|
94
94
|
description,
|
|
95
95
|
stainlessPath,
|
|
96
|
-
qualified: decl['qualified'],
|
|
97
|
-
ident: decl['ident'],
|
|
96
|
+
qualified: 'qualified' in decl ? decl['qualified'] : undefined,
|
|
97
|
+
ident: 'ident' in decl ? decl['ident'] : undefined,
|
|
98
98
|
content: chunk,
|
|
99
99
|
url: generateRoute('docs://BASE_PATH', language, stainlessPath),
|
|
100
100
|
};
|
|
@@ -105,7 +105,7 @@ export function* generateChatIndex(spec: SDKJSON.Spec) {
|
|
|
105
105
|
|
|
106
106
|
export function* generateIndex(
|
|
107
107
|
spec: SDKJSON.Spec,
|
|
108
|
-
renderMarkdownFn?: (string
|
|
108
|
+
renderMarkdownFn?: (_: string) => string | null,
|
|
109
109
|
includeTypes?: boolean,
|
|
110
110
|
): Generator<IndexEntry> {
|
|
111
111
|
const parentCrumbs: Record<string, string[]> = {};
|
|
@@ -113,8 +113,8 @@ export function* generateIndex(
|
|
|
113
113
|
const { kind, name, title, stainlessPath } = data;
|
|
114
114
|
const common = { name, title, stainlessPath };
|
|
115
115
|
|
|
116
|
-
const parsedPath = parseStainlessPath(stainlessPath)
|
|
117
|
-
const crumbs = getResourceNames(parsedPath.resource
|
|
116
|
+
const parsedPath = parseStainlessPath(stainlessPath)!;
|
|
117
|
+
const crumbs = getResourceNames(parsedPath.resource!, spec.resources);
|
|
118
118
|
|
|
119
119
|
switch (kind) {
|
|
120
120
|
case 'resource':
|
|
@@ -144,7 +144,7 @@ export function* generateIndex(
|
|
|
144
144
|
if (!found) continue;
|
|
145
145
|
|
|
146
146
|
parentCrumbs[stainlessPath] = [...crumbs, title];
|
|
147
|
-
const qualified = found['qualified'];
|
|
147
|
+
const qualified = 'qualified' in found ? found['qualified'] : undefined;
|
|
148
148
|
const ident = qualified?.split('.')?.at(-1);
|
|
149
149
|
|
|
150
150
|
yield {
|
|
@@ -153,7 +153,9 @@ export function* generateIndex(
|
|
|
153
153
|
ident,
|
|
154
154
|
qualified,
|
|
155
155
|
language,
|
|
156
|
-
description:
|
|
156
|
+
description: data.description
|
|
157
|
+
? (renderMarkdownFn?.(data.description) ?? data.description)
|
|
158
|
+
: undefined,
|
|
157
159
|
endpoint: endpoint.slice(httpMethod.length).trim(),
|
|
158
160
|
httpMethod,
|
|
159
161
|
summary,
|
|
@@ -172,9 +174,9 @@ export function* generateIndex(
|
|
|
172
174
|
parentCrumbs[stainlessPath] = [...crumbs, title];
|
|
173
175
|
const schema = spec.decls[language]?.[`${stainlessPath} > (schema)`];
|
|
174
176
|
const children =
|
|
175
|
-
schema?.['children']
|
|
177
|
+
(schema && 'children' in schema ? schema?.['children'] : undefined)
|
|
176
178
|
?.map((childPath) => {
|
|
177
|
-
const child = spec.decls?.[language]?.[childPath];
|
|
179
|
+
const child = spec.decls?.[language]?.[childPath] as any;
|
|
178
180
|
return (
|
|
179
181
|
child?.['ident'] ??
|
|
180
182
|
child?.['name'] ??
|
|
@@ -192,7 +194,7 @@ export function* generateIndex(
|
|
|
192
194
|
children,
|
|
193
195
|
language,
|
|
194
196
|
priority: 2,
|
|
195
|
-
ident: schema?.['ident'],
|
|
197
|
+
ident: schema && 'ident' in schema ? schema?.['ident'] : undefined,
|
|
196
198
|
...common,
|
|
197
199
|
};
|
|
198
200
|
}
|
|
@@ -213,11 +215,11 @@ export function* generateIndex(
|
|
|
213
215
|
case 'HttpDeclProperty':
|
|
214
216
|
case 'TSDeclProperty':
|
|
215
217
|
{
|
|
216
|
-
const parsedPath = parseStainlessPath(decl.stainlessPath)
|
|
218
|
+
const parsedPath = parseStainlessPath(decl.stainlessPath)!;
|
|
217
219
|
const type = includeTypes === false ? undefined : printer.typeName(language, decl.type);
|
|
218
|
-
const name = decl['ident'] ?? decl['name'] ?? decl['key'];
|
|
220
|
+
const name: string = (decl as any)['ident'] ?? (decl as any)['name'] ?? (decl as any)['key'];
|
|
219
221
|
|
|
220
|
-
const parent = parentCrumbs[parsedPath.routable];
|
|
222
|
+
const parent = parentCrumbs[parsedPath.routable!];
|
|
221
223
|
// Filter out properties of non-routable response types
|
|
222
224
|
if (parent === undefined) continue;
|
|
223
225
|
|
|
@@ -232,7 +234,7 @@ export function* generateIndex(
|
|
|
232
234
|
name,
|
|
233
235
|
stainlessPath: decl.stainlessPath,
|
|
234
236
|
crumbs: [...parent, ...props],
|
|
235
|
-
docstring: renderMarkdownFn?.(decl.docstring) ?? decl.docstring,
|
|
237
|
+
docstring: decl.docstring ? (renderMarkdownFn?.(decl.docstring) ?? decl.docstring) : undefined,
|
|
236
238
|
type,
|
|
237
239
|
language,
|
|
238
240
|
priority: 3,
|
package/src/search/mcp.ts
CHANGED
|
@@ -1,28 +1,38 @@
|
|
|
1
|
+
import { BM25Retriever } from '@langchain/community/retrievers/bm25';
|
|
1
2
|
import { renderMarkdown } from '../markdown';
|
|
2
3
|
import { DocsLanguage, parseStainlessPath } from '../routing';
|
|
3
4
|
import { getResourceFromSpec } from '../utils';
|
|
4
|
-
import { buildIndex, search } from './providers/fuse';
|
|
5
|
-
import type { IndexEntry } from './types';
|
|
6
|
-
import
|
|
5
|
+
import { buildIndex, search as fuseSearch } from './providers/fuse';
|
|
6
|
+
import type { IndexEntry, IndexMethod } from './types';
|
|
7
|
+
import natural from 'natural';
|
|
8
|
+
import type * as SDKJSON from '@stainless/sdk-json';
|
|
9
|
+
|
|
10
|
+
type Item = IndexEntry & IndexMethod;
|
|
11
|
+
|
|
12
|
+
export interface SearchResult {
|
|
13
|
+
item: Item;
|
|
14
|
+
score?: number;
|
|
15
|
+
refIndex: number;
|
|
16
|
+
}
|
|
7
17
|
|
|
8
18
|
export function consolidate(results: IndexEntry[]) {
|
|
9
19
|
const resources = new Set<string>();
|
|
10
20
|
const methods = new Set<string>();
|
|
11
21
|
|
|
12
22
|
for (const entry of results) {
|
|
13
|
-
const parsed = parseStainlessPath(entry.stainlessPath)
|
|
14
|
-
if (parsed.method) methods.add(parsed.routable);
|
|
15
|
-
else resources.add(parsed.routable);
|
|
23
|
+
const parsed = parseStainlessPath(entry.stainlessPath)!;
|
|
24
|
+
if (parsed.method) methods.add(parsed.routable!);
|
|
25
|
+
else resources.add(parsed.routable!);
|
|
16
26
|
}
|
|
17
27
|
|
|
18
|
-
const filtered = Array.from(methods).filter((path) => !resources.has(path.split(' >').at(0)));
|
|
28
|
+
const filtered = Array.from(methods).filter((path) => !resources.has(path.split(' >').at(0)!));
|
|
19
29
|
return [...resources, ...filtered];
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
export function render(
|
|
23
33
|
spec: SDKJSON.Spec,
|
|
24
34
|
language: DocsLanguage,
|
|
25
|
-
items:
|
|
35
|
+
items: Item[],
|
|
26
36
|
includeModelProperties: boolean,
|
|
27
37
|
) {
|
|
28
38
|
const env = {
|
|
@@ -36,9 +46,9 @@ export function render(
|
|
|
36
46
|
|
|
37
47
|
const paths = consolidate(items);
|
|
38
48
|
const output = paths.map((entry) => {
|
|
39
|
-
const parsed = parseStainlessPath(entry)
|
|
40
|
-
const resource = getResourceFromSpec(parsed.resource
|
|
41
|
-
const target = parsed.method ? resource.methods[parsed.method] : resource;
|
|
49
|
+
const parsed = parseStainlessPath(entry)!;
|
|
50
|
+
const resource = getResourceFromSpec(parsed.resource!, spec)!;
|
|
51
|
+
const target = parsed.method ? resource.methods[parsed.method]! : resource;
|
|
42
52
|
const content = renderMarkdown(env, target);
|
|
43
53
|
return [entry, content];
|
|
44
54
|
});
|
|
@@ -46,16 +56,98 @@ export function render(
|
|
|
46
56
|
return Object.fromEntries(output);
|
|
47
57
|
}
|
|
48
58
|
|
|
49
|
-
export function searchAndRender(
|
|
59
|
+
export async function searchAndRender(
|
|
50
60
|
spec: SDKJSON.Spec,
|
|
51
61
|
language: DocsLanguage,
|
|
52
62
|
query: string,
|
|
53
63
|
limit?: number,
|
|
54
64
|
includeModelProperties: boolean = false,
|
|
55
65
|
) {
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
return render(spec, language,
|
|
66
|
+
const results = await search(spec, language, query, limit);
|
|
67
|
+
const items = results.map(({ item }) => item);
|
|
68
|
+
return render(spec, language, items, includeModelProperties);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function search(
|
|
72
|
+
spec: SDKJSON.Spec,
|
|
73
|
+
language: DocsLanguage,
|
|
74
|
+
query: string,
|
|
75
|
+
limit: number = 100,
|
|
76
|
+
): Promise<SearchResult[]> {
|
|
77
|
+
const fuseIndex = buildIndex(spec, language);
|
|
78
|
+
|
|
79
|
+
// only HTTP methods are useful for MCP
|
|
80
|
+
const httpMethodEntries = fuseIndex.content.filter((entry) => entry.kind === 'http_method') as Item[];
|
|
81
|
+
|
|
82
|
+
// build BM25 retriever if we have HTTP methods
|
|
83
|
+
let bm25Retriever: BM25Retriever | null = null;
|
|
84
|
+
if (httpMethodEntries.length > 0) {
|
|
85
|
+
const documents = httpMethodEntries.map((entry) => {
|
|
86
|
+
// empirically, endpoint + summary seems to be a reasonable semantic encapsulation of what the endpoint does
|
|
87
|
+
const content = `${entry.endpoint} ${entry.summary}`;
|
|
88
|
+
return {
|
|
89
|
+
// stem the content - we will be stemming the query as well
|
|
90
|
+
pageContent: stemText(content),
|
|
91
|
+
metadata: {
|
|
92
|
+
stainlessPath: entry.stainlessPath,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
bm25Retriever = BM25Retriever.fromDocuments(documents, { k: 100 });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// get initial results from Fuse
|
|
101
|
+
const rawResults = fuseSearch(fuseIndex, query, 100).filter(
|
|
102
|
+
(r) => r.item.kind === 'http_method',
|
|
103
|
+
) as SearchResult[];
|
|
104
|
+
|
|
105
|
+
// only keep HTTP methods
|
|
106
|
+
const filtered = rawResults.filter((r) => r.item.kind === 'http_method');
|
|
107
|
+
|
|
108
|
+
// stem the query and apply BM25 reranking
|
|
109
|
+
if (filtered.length > 0 && bm25Retriever) {
|
|
110
|
+
const stemmedQuery = stemText(query);
|
|
111
|
+
|
|
112
|
+
// get BM25-ranked result
|
|
113
|
+
const results = await bm25Retriever.invoke(stemmedQuery);
|
|
114
|
+
const reranked = results.map((doc) => doc.metadata);
|
|
115
|
+
|
|
116
|
+
// build a map of Fuse results by path for fast lookup
|
|
117
|
+
const sortStart = Date.now();
|
|
118
|
+
const fuseResultsByPath = new Map(filtered.map((r) => [r.item.stainlessPath, r]));
|
|
119
|
+
|
|
120
|
+
// use BM25 ordering to reorder Fuse results
|
|
121
|
+
const reorderedResults: SearchResult[] = [];
|
|
122
|
+
for (const doc of reranked) {
|
|
123
|
+
const fuseResult = fuseResultsByPath.get(doc.stainlessPath);
|
|
124
|
+
if (fuseResult) {
|
|
125
|
+
reorderedResults.push(fuseResult);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// replace filtered with reordered results
|
|
130
|
+
filtered.length = 0;
|
|
131
|
+
filtered.push(...reorderedResults);
|
|
132
|
+
console.debug(` [SORT] Reranked in ${Date.now() - sortStart}ms`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return filtered.slice(0, limit);
|
|
59
136
|
}
|
|
60
137
|
|
|
61
|
-
|
|
138
|
+
function stemText(text: string): string {
|
|
139
|
+
if (!text) return '';
|
|
140
|
+
|
|
141
|
+
// tokenize manually and stem each word
|
|
142
|
+
const words = text.toLowerCase().split(/\s+/);
|
|
143
|
+
const stemmedWords = words
|
|
144
|
+
.map((word) => {
|
|
145
|
+
// remove punctuation and stem
|
|
146
|
+
const cleaned = word.replace(/[^\w]/g, '');
|
|
147
|
+
if (!cleaned) return '';
|
|
148
|
+
return natural.LancasterStemmer.stem(cleaned);
|
|
149
|
+
})
|
|
150
|
+
.filter((w) => w);
|
|
151
|
+
|
|
152
|
+
return stemmedWords.join(' ');
|
|
153
|
+
}
|
package/src/search/printer.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
3
|
-
import type * as SDKJSON from '
|
|
3
|
+
import type * as SDKJSON from '@stainless/sdk-json';
|
|
4
4
|
import type { DocsLanguage } from '../routing';
|
|
5
5
|
|
|
6
6
|
import { DocsProvider, useLanguageComponents } from '../contexts';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { searchClient } from '@algolia/client-search';
|
|
2
2
|
import { generateChatIndex, generateIndex } from '../indexer';
|
|
3
3
|
import { SearchableAttributes, SearchableAttributesChat } from '../types';
|
|
4
|
-
import type * as SDKJSON from '
|
|
4
|
+
import type * as SDKJSON from '@stainless/sdk-json';
|
|
5
5
|
import type { ResultRecordType, SearchSettings, SearchParams, ResultType } from '../types';
|
|
6
6
|
|
|
7
7
|
export async function buildIndex(
|
|
@@ -9,7 +9,7 @@ export async function buildIndex(
|
|
|
9
9
|
indexName: string,
|
|
10
10
|
writeKey: string,
|
|
11
11
|
spec: SDKJSON.Spec,
|
|
12
|
-
renderMarkdown: (string
|
|
12
|
+
renderMarkdown: (_: string) => string | null,
|
|
13
13
|
): Promise<void> {
|
|
14
14
|
if (!appId || !indexName || !writeKey) return;
|
|
15
15
|
const objects = Array.from(generateIndex(spec, renderMarkdown));
|
|
@@ -22,7 +22,7 @@ export async function buildIndex(
|
|
|
22
22
|
highlightPostTag: '</mark>',
|
|
23
23
|
customRanking: ['asc(priority)'],
|
|
24
24
|
attributesForFaceting: ['language', 'kind'],
|
|
25
|
-
searchableAttributes: SearchableAttributes,
|
|
25
|
+
searchableAttributes: [...SearchableAttributes],
|
|
26
26
|
},
|
|
27
27
|
});
|
|
28
28
|
|
|
@@ -81,8 +81,8 @@ export async function search({
|
|
|
81
81
|
],
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
if ('hits' in results[0] && 'hits' in results[1]) {
|
|
84
|
+
if ('hits' in results[0]! && 'hits' in results[1]!) {
|
|
85
85
|
const [{ nbHits, facets }, { hits }] = results;
|
|
86
|
-
return { hits, nbHits, facets };
|
|
86
|
+
return { hits, nbHits: nbHits ?? 0, facets };
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -2,18 +2,18 @@ import Fuse, { FuseIndex } from 'fuse.js';
|
|
|
2
2
|
import { DocsLanguage } from '../../routing';
|
|
3
3
|
import { generateIndex } from '../indexer';
|
|
4
4
|
import { IndexEntry, SearchableAttributes } from '../types';
|
|
5
|
-
import type * as SDKJSON from '
|
|
5
|
+
import type * as SDKJSON from '@stainless/sdk-json';
|
|
6
6
|
|
|
7
7
|
export type FuseIndexData = { content: IndexEntry[]; index: FuseIndex<IndexEntry> };
|
|
8
8
|
|
|
9
9
|
export function buildIndex(spec: SDKJSON.Spec, language?: DocsLanguage): FuseIndexData {
|
|
10
|
-
const idx = Array.from(generateIndex(spec,
|
|
10
|
+
const idx = Array.from(generateIndex(spec, undefined, false));
|
|
11
11
|
const content = language ? idx.filter((entry) => entry.language === language) : idx;
|
|
12
|
-
const index = Fuse.createIndex(SearchableAttributes, content);
|
|
12
|
+
const index = Fuse.createIndex([...SearchableAttributes], content);
|
|
13
13
|
return { content, index };
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function search({ content, index }: FuseIndexData, query: string, limit: number = 100) {
|
|
17
|
-
const fuse = new Fuse(content, { keys: SearchableAttributes }, index);
|
|
17
|
+
const fuse = new Fuse(content, { keys: [...SearchableAttributes] }, index);
|
|
18
18
|
return fuse.search(query).slice(0, limit);
|
|
19
19
|
}
|
|
@@ -13,5 +13,5 @@ export async function guideSearch(
|
|
|
13
13
|
const index = await loadPagefind(loadPath);
|
|
14
14
|
const response = await index.search(query);
|
|
15
15
|
const items = limit ? response.results.slice(0, limit) : response.results;
|
|
16
|
-
return Promise.all(items.map((result) => result.data().then((data) => ({ ...result, data }))));
|
|
16
|
+
return Promise.all(items.map((result: any) => result.data().then((data: any) => ({ ...result, data }))));
|
|
17
17
|
}
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { DocsLanguage } from '../../routing';
|
|
2
2
|
import { generateIndex } from '../indexer';
|
|
3
3
|
import { IndexEntry, SearchableAttributes } from '../types';
|
|
4
|
-
import type * as SDKJSON from '
|
|
4
|
+
import type * as SDKJSON from '@stainless/sdk-json';
|
|
5
5
|
|
|
6
6
|
export function buildIndex(spec: SDKJSON.Spec) {
|
|
7
|
-
return generateIndex(spec,
|
|
7
|
+
return generateIndex(spec, undefined, false);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
function* findEntryInIndex(index: Generator<IndexEntry>, language: string, query: string) {
|
|
11
11
|
for (const entry of index) {
|
|
12
12
|
if (entry.language !== language) continue;
|
|
13
13
|
for (const attr of SearchableAttributes) {
|
|
14
|
-
|
|
14
|
+
const attr_ = attr in entry ? (attr as keyof typeof entry) : null;
|
|
15
|
+
if (attr_ && entry[attr_] && typeof entry[attr_] === 'string' && entry[attr_].includes(query))
|
|
16
|
+
yield entry;
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
19
|
}
|
package/src/search/results.tsx
CHANGED
|
@@ -109,6 +109,7 @@ export function SearchResultContent({ result }: SearchResultProps) {
|
|
|
109
109
|
return (
|
|
110
110
|
<>
|
|
111
111
|
<Docs.MethodHeader
|
|
112
|
+
level="h5"
|
|
112
113
|
title={<Highlight result={result} name={result.summary ? 'summary' : 'title'} />}
|
|
113
114
|
signature={result['qualified'] && <Highlight result={result} name={'qualified'} />}
|
|
114
115
|
>
|
|
@@ -117,18 +118,19 @@ export function SearchResultContent({ result }: SearchResultProps) {
|
|
|
117
118
|
endpoint={<Highlight result={result} name="endpoint" />}
|
|
118
119
|
/>
|
|
119
120
|
</Docs.MethodHeader>
|
|
120
|
-
<div className={style.MethodDescription}>
|
|
121
|
+
<div className={`${style.MethodDescription} ${style.Content}`}>
|
|
121
122
|
<Highlight result={result} name="description" />
|
|
122
123
|
</div>
|
|
123
124
|
</>
|
|
124
125
|
);
|
|
125
126
|
|
|
126
127
|
case 'model': {
|
|
127
|
-
const properties =
|
|
128
|
-
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
const properties =
|
|
129
|
+
result.children?.map((child, index) => (
|
|
130
|
+
<span key={index} className={style.TextIdentifier}>
|
|
131
|
+
{child}
|
|
132
|
+
</span>
|
|
133
|
+
)) ?? [];
|
|
132
134
|
|
|
133
135
|
return (
|
|
134
136
|
<div className={style.Property} data-stldocs-language={language}>
|
|
@@ -167,7 +169,7 @@ export function SearchResultContent({ result }: SearchResultProps) {
|
|
|
167
169
|
<Highlight result={result} name="name" />
|
|
168
170
|
</span>
|
|
169
171
|
<span className={style.PropertyTypeName}>
|
|
170
|
-
<span dangerouslySetInnerHTML={{ __html: result.type }} />
|
|
172
|
+
<span dangerouslySetInnerHTML={{ __html: result.type ?? '' }} />
|
|
171
173
|
</span>
|
|
172
174
|
</div>
|
|
173
175
|
{result.docstring && (
|
package/src/search/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type * as SDKJSON from '
|
|
1
|
+
import type * as SDKJSON from '@stainless/sdk-json';
|
|
2
2
|
import { DocsLanguage } from '../routing';
|
|
3
3
|
|
|
4
4
|
export type SearchSettings = {
|
|
@@ -58,7 +58,7 @@ export const SearchableAttributes = [
|
|
|
58
58
|
'summary',
|
|
59
59
|
'description',
|
|
60
60
|
'docstring',
|
|
61
|
-
];
|
|
61
|
+
] as const;
|
|
62
62
|
|
|
63
63
|
export const SearchableAttributesChat = [
|
|
64
64
|
'title',
|
package/src/style.ts
CHANGED
|
@@ -55,7 +55,6 @@ export default {
|
|
|
55
55
|
SnippetRequestTitleLabel: 'stldocs-snippet-request-title-label',
|
|
56
56
|
SnippetRequestTitleLanguage: 'stldocs-snippet-request-title-language',
|
|
57
57
|
SnippetRequestTitleMethod: 'stldocs-snippet-request-title-method',
|
|
58
|
-
SnippetRequestTitleCopyButton: 'stldocs-snippet-request-title-copy-button',
|
|
59
58
|
|
|
60
59
|
SnippetResponse: 'stldocs-snippet-response',
|
|
61
60
|
SnippetMultiResponse: 'stldocs-snippet-multi-response',
|
|
@@ -151,6 +150,7 @@ export default {
|
|
|
151
150
|
SearchForm: 'stldocs-search-form',
|
|
152
151
|
SearchModal: 'stldocs-search-modal',
|
|
153
152
|
SearchFilter: 'stldocs-search-filter',
|
|
153
|
+
SearchFilterLabel: 'stldocs-search-filter-label',
|
|
154
154
|
SearchFilterCount: 'stldocs-search-filter-count',
|
|
155
155
|
SearchBreadcrumb: 'stldocs-search-breadcrumb',
|
|
156
156
|
SearchBreadcrumbItem: 'stldocs-search-breadcrumb-item',
|