@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.
Files changed (57) hide show
  1. package/dist/index.js +1312 -1871
  2. package/dist/mcp.cjs +983441 -0
  3. package/dist/routing.js +4 -4
  4. package/dist/styles/main.css +743 -747
  5. package/dist/styles/primitives.css +444 -426
  6. package/dist/styles/resets.css +33 -41
  7. package/dist/styles/search.css +265 -248
  8. package/dist/styles/sidebar.css +58 -60
  9. package/dist/styles/snippets.css +86 -88
  10. package/dist/styles/variables.css +85 -89
  11. package/package.json +19 -10
  12. package/src/components/breadcrumbs.tsx +1 -1
  13. package/src/components/chat.tsx +18 -15
  14. package/src/components/method.tsx +12 -11
  15. package/src/components/overview.tsx +32 -19
  16. package/src/components/primitives.tsx +36 -19
  17. package/src/components/properties.tsx +4 -2
  18. package/src/components/scripts/dropdown.ts +1 -1
  19. package/src/components/sdk.tsx +28 -22
  20. package/src/components/sidebar.tsx +3 -3
  21. package/src/components/snippets.tsx +29 -11
  22. package/src/contexts/component-generics.tsx +10 -15
  23. package/src/contexts/docs.tsx +15 -4
  24. package/src/contexts/index.tsx +8 -5
  25. package/src/contexts/markdown.tsx +7 -6
  26. package/src/contexts/search.tsx +4 -5
  27. package/src/hooks/use-strict-context.tsx +16 -0
  28. package/src/languages/go.tsx +3 -3
  29. package/src/languages/http.tsx +31 -23
  30. package/src/languages/index.ts +7 -7
  31. package/src/languages/java.tsx +4 -4
  32. package/src/languages/python.tsx +12 -9
  33. package/src/languages/ruby.tsx +20 -13
  34. package/src/languages/typescript.tsx +18 -12
  35. package/src/markdown/index.ts +17 -12
  36. package/src/markdown/utils.ts +6 -3
  37. package/src/routing.ts +9 -9
  38. package/src/search/form.tsx +11 -8
  39. package/src/search/indexer.ts +17 -15
  40. package/src/search/mcp.ts +108 -16
  41. package/src/search/printer.tsx +1 -1
  42. package/src/search/providers/algolia.ts +5 -5
  43. package/src/search/providers/fuse.ts +4 -4
  44. package/src/search/providers/pagefind.ts +1 -1
  45. package/src/search/providers/walker.ts +5 -3
  46. package/src/search/results.tsx +9 -7
  47. package/src/search/types.ts +2 -2
  48. package/src/style.ts +1 -1
  49. package/src/styles/main.css +743 -747
  50. package/src/styles/primitives.css +444 -426
  51. package/src/styles/resets.css +33 -41
  52. package/src/styles/search.css +265 -248
  53. package/src/styles/sidebar.css +58 -60
  54. package/src/styles/snippets.css +86 -88
  55. package/src/styles/variables.css +85 -89
  56. package/src/utils.ts +14 -15
  57. package/dist/mcp.js +0 -16003
@@ -1,6 +1,6 @@
1
1
  import { Parser } from 'htmlparser2';
2
2
  import type { DocsLanguage } from '../routing';
3
- import type * as SDKJSON from '~/lib/json-spec-v2/types';
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 = decl['type']['$ref'];
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 '~/lib/json-spec-v2/types';
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.split('.'),
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.flatMap((name, index) => [
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.length
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.tagName === 'DETAILS') el['open'] = true;
222
+ if (el instanceof HTMLDetailsElement) el.open = true;
223
223
  el = el.parentElement;
224
224
  }
225
225
  }
@@ -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.hits],
32
+ items: filterKind === 'guide' ? guideResults : [...guideResults, ...(apiResults?.hits ?? [])],
33
33
  counts: {
34
- ...apiResults.facets?.['kind'],
34
+ ...apiResults?.facets?.['kind'],
35
35
  guide: guideResults.length,
36
- all: apiResults.nbHits,
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) => onSelect?.(item['data']?.['url'] ?? item['stainlessPath'])}
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?: QueryKindsType) => void;
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
@@ -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 '~/lib/json-spec-v2/types';
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?) => string | null,
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, spec.resources);
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: renderMarkdownFn?.(data.description) ?? data.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 type * as SDKJSON from '~/lib/json-spec-v2/types';
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: IndexEntry[],
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, spec);
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 idx = buildIndex(spec, language);
57
- const results = search(idx, query, limit).map(({ item }) => item);
58
- return render(spec, language, results, includeModelProperties);
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
- export { buildIndex };
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
+ }
@@ -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 '~/lib/json-spec-v2/types';
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 '~/lib/json-spec-v2/types';
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?) => string | null,
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 '~/lib/json-spec-v2/types';
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, null, false));
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 '~/lib/json-spec-v2/types';
4
+ import type * as SDKJSON from '@stainless/sdk-json';
5
5
 
6
6
  export function buildIndex(spec: SDKJSON.Spec) {
7
- return generateIndex(spec, null, false);
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
- if (entry[attr] && typeof entry[attr] === 'string' && entry[attr].includes(query)) yield entry;
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
  }
@@ -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 = result.children.map((child, index) => (
128
- <span key={index} className={style.TextIdentifier}>
129
- {child}
130
- </span>
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 && (
@@ -1,4 +1,4 @@
1
- import type * as SDKJSON from '~/lib/json-spec-v2/types';
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',