@gsa-tts/graymatter-ui 0.3.14 → 0.3.15
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/README.md +8 -4
- package/package.json +6 -3
- package/src/components/ApiDocSubNavList.svelte +25 -0
- package/src/components/ApiDocSubNavMenu.svelte +464 -0
- package/src/components/ApiDocSubNavMenuItem.svelte +116 -0
- package/src/components/index.ts +3 -0
- package/src/content/api/endpoints.mdx +174 -0
- package/src/content/api/getting-started.mdx +84 -0
- package/src/content/api/support.mdx +10 -0
- package/src/layouts/DocsLayout.astro +178 -0
- package/src/types/subnav.ts +15 -0
- package/src/utils/copyButtonScript.ts +107 -0
- package/src/utils/generateApiNavData.ts +113 -0
- package/src/utils/index.ts +2 -0
package/README.md
CHANGED
|
@@ -190,6 +190,7 @@ import {
|
|
|
190
190
|
### Layout Components
|
|
191
191
|
- **GlobalAppLayout** - Main application layout wrapper
|
|
192
192
|
- **BaseLayout** - Base page layout structure
|
|
193
|
+
- **DocsLayout** - Documentation layout with dynamic navigation (Astro only)
|
|
193
194
|
|
|
194
195
|
### UI Components
|
|
195
196
|
- **ProfileMenu** - User profile dropdown menu with event-driven actions
|
|
@@ -235,15 +236,18 @@ Detailed documentation for specific components is available in the package:
|
|
|
235
236
|
|
|
236
237
|
- **[NAVIGATION_CONTROL.md](https://github.com/GSA-TTS/usai-gov/blob/main/packages/ui/NAVIGATION_CONTROL.md)** - DesktopSideNav external control system with event-based API
|
|
237
238
|
- **[PROFILE_MENU.md](https://github.com/GSA-TTS/usai-gov/blob/main/packages/ui/PROFILE_MENU.md)** - ProfileMenu component usage and event handling
|
|
239
|
+
- **[README_DocsLayout.md](https://github.com/GSA-TTS/usai-gov/blob/main/packages/ui/README_DocsLayout.md)** - DocsLayout component with dynamic navigation generation (Astro only)
|
|
238
240
|
|
|
239
241
|
These files are also included with the package in your `node_modules/@gsa-tts/graymatter-ui/` directory. If you don't have access to the private repository, you can find the documentation files locally after installation.
|
|
240
242
|
|
|
241
243
|
## Framework Support
|
|
242
244
|
|
|
243
|
-
- **Svelte 5** - Native Svelte components
|
|
244
|
-
- **Astro** - Full support with client-side hydration
|
|
245
|
-
- **React/Vue/Angular** - Via web components
|
|
246
|
-
- **Vanilla JavaScript** - Via web components
|
|
245
|
+
- **Svelte 5** - Native Svelte components (individual components only)
|
|
246
|
+
- **Astro** - Full support with client-side hydration (including DocsLayout)
|
|
247
|
+
- **React/Vue/Angular** - Via web components (individual components only)
|
|
248
|
+
- **Vanilla JavaScript** - Via web components (individual components only)
|
|
249
|
+
|
|
250
|
+
**Note:** The `DocsLayout` component is Astro-specific and cannot be used in other frameworks. For non-Astro applications, use the individual Svelte components and shared utilities instead.
|
|
247
251
|
|
|
248
252
|
## Troubleshooting
|
|
249
253
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gsa-tts/graymatter-ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
"./components": "./src/components/index.ts",
|
|
19
19
|
"./components/*": "./src/components/*",
|
|
20
20
|
"./constants": "./src/constants/index.ts",
|
|
21
|
+
"./data": "./src/data/index.ts",
|
|
22
|
+
"./data/*": "./src/data/*",
|
|
23
|
+
"./content/api/*": "./src/content/api/*",
|
|
21
24
|
"./helpers": "./src/helpers/index.ts",
|
|
22
25
|
"./helpers/*": "./src/helpers/*",
|
|
23
26
|
"./utils": "./src/utils/index.ts",
|
|
@@ -55,9 +58,9 @@
|
|
|
55
58
|
"typescript": "5.7.3",
|
|
56
59
|
"vite": "^6.3.5",
|
|
57
60
|
"vitest": "^3.0.7",
|
|
58
|
-
"@gsa-tts/graymatter-
|
|
61
|
+
"@gsa-tts/graymatter-typescript-config": "0.0.2",
|
|
59
62
|
"@gsa-tts/graymatter-vitest-config": "0.0.1",
|
|
60
|
-
"@gsa-tts/graymatter-
|
|
63
|
+
"@gsa-tts/graymatter-eslint": "0.0.2"
|
|
61
64
|
},
|
|
62
65
|
"dependencies": {
|
|
63
66
|
"@fontsource-variable/archivo": "^5.2.6",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import ApiDocSubNavMenuItem from './ApiDocSubNavMenuItem.svelte';
|
|
3
|
+
import Self from './ApiDocSubNavList.svelte';
|
|
4
|
+
|
|
5
|
+
const { items, level = 0, selectedId, onClick, onKeyDown } = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
{#each items as item}
|
|
9
|
+
<ApiDocSubNavMenuItem
|
|
10
|
+
{item}
|
|
11
|
+
{level}
|
|
12
|
+
isSelected={selectedId === item.id}
|
|
13
|
+
{onClick}
|
|
14
|
+
{onKeyDown}
|
|
15
|
+
/>
|
|
16
|
+
{#if item.children}
|
|
17
|
+
<Self
|
|
18
|
+
items={item.children}
|
|
19
|
+
level={level + 1}
|
|
20
|
+
{selectedId}
|
|
21
|
+
{onClick}
|
|
22
|
+
{onKeyDown}
|
|
23
|
+
/>
|
|
24
|
+
{/if}
|
|
25
|
+
{/each}
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { createEventDispatcher, onMount } from 'svelte';
|
|
3
|
+
import { subNavReady } from '@gsa-tts/graymatter-ui/stores/subNavReadyStore';
|
|
4
|
+
import type { SubNavData, SubNavItem } from '../types/subnav.ts';
|
|
5
|
+
import ApiDocSubNavList from './ApiDocSubNavList.svelte';
|
|
6
|
+
|
|
7
|
+
const { data = null } = $props();
|
|
8
|
+
|
|
9
|
+
const sections = $derived(() =>
|
|
10
|
+
!Array.isArray(data) &&
|
|
11
|
+
data &&
|
|
12
|
+
typeof data === 'object' &&
|
|
13
|
+
'sections' in data &&
|
|
14
|
+
Array.isArray(
|
|
15
|
+
(data as { sections: { header: string; items: SubNavItem[] }[] }).sections
|
|
16
|
+
)
|
|
17
|
+
? (data as { sections: { header: string; items: SubNavItem[] }[] })
|
|
18
|
+
.sections
|
|
19
|
+
: null
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const dispatch = createEventDispatcher<{
|
|
23
|
+
navigate: { item: SubNavItem };
|
|
24
|
+
itemSelect: { item: SubNavItem };
|
|
25
|
+
mobileAnchorNavigate: { hash: string };
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
function findFirstItem(data: SubNavData | null): SubNavItem | null {
|
|
29
|
+
if (!data) return null;
|
|
30
|
+
|
|
31
|
+
if (Array.isArray(data)) {
|
|
32
|
+
return data[0] || null;
|
|
33
|
+
} else if (
|
|
34
|
+
data &&
|
|
35
|
+
typeof data === 'object' &&
|
|
36
|
+
'sections' in data &&
|
|
37
|
+
Array.isArray(
|
|
38
|
+
(data as { sections: { header: string; items: SubNavItem[] }[] })
|
|
39
|
+
.sections
|
|
40
|
+
)
|
|
41
|
+
) {
|
|
42
|
+
const sections = (
|
|
43
|
+
data as { sections: { header: string; items: SubNavItem[] }[] }
|
|
44
|
+
).sections;
|
|
45
|
+
if (
|
|
46
|
+
sections.length > 0 &&
|
|
47
|
+
sections[0] &&
|
|
48
|
+
sections[0].items &&
|
|
49
|
+
sections[0].items.length > 0
|
|
50
|
+
) {
|
|
51
|
+
return sections[0].items[0] || null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function findItemByHash(
|
|
58
|
+
data: SubNavData | null,
|
|
59
|
+
hash: string
|
|
60
|
+
): SubNavItem | null {
|
|
61
|
+
if (!data) return null;
|
|
62
|
+
|
|
63
|
+
function searchInItems(items: SubNavItem[]): SubNavItem | null {
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
if (item.href && item.href.includes('#')) {
|
|
66
|
+
const itemHash = item.href.split('#')[1];
|
|
67
|
+
if (itemHash === hash) {
|
|
68
|
+
return item;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (item.children) {
|
|
72
|
+
const found = searchInItems(item.children);
|
|
73
|
+
if (found) return found;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (Array.isArray(data)) {
|
|
80
|
+
return searchInItems(data);
|
|
81
|
+
} else if (
|
|
82
|
+
data &&
|
|
83
|
+
typeof data === 'object' &&
|
|
84
|
+
'sections' in data &&
|
|
85
|
+
Array.isArray(
|
|
86
|
+
(data as { sections: { header: string; items: SubNavItem[] }[] })
|
|
87
|
+
.sections
|
|
88
|
+
)
|
|
89
|
+
) {
|
|
90
|
+
for (const section of (
|
|
91
|
+
data as { sections: { header: string; items: SubNavItem[] }[] }
|
|
92
|
+
).sections) {
|
|
93
|
+
const found = searchInItems(section.items);
|
|
94
|
+
if (found) return found;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function findItemById(
|
|
101
|
+
data: SubNavData | null,
|
|
102
|
+
id: string
|
|
103
|
+
): SubNavItem | null {
|
|
104
|
+
if (!data) return null;
|
|
105
|
+
|
|
106
|
+
function searchInItems(items: SubNavItem[]): SubNavItem | null {
|
|
107
|
+
for (const item of items) {
|
|
108
|
+
if (item.id === id) {
|
|
109
|
+
return item;
|
|
110
|
+
}
|
|
111
|
+
if (item.children) {
|
|
112
|
+
const found = searchInItems(item.children);
|
|
113
|
+
if (found) return found;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (Array.isArray(data)) {
|
|
120
|
+
return searchInItems(data);
|
|
121
|
+
} else if (
|
|
122
|
+
data &&
|
|
123
|
+
typeof data === 'object' &&
|
|
124
|
+
'sections' in data &&
|
|
125
|
+
Array.isArray(
|
|
126
|
+
(data as { sections: { header: string; items: SubNavItem[] }[] })
|
|
127
|
+
.sections
|
|
128
|
+
)
|
|
129
|
+
) {
|
|
130
|
+
for (const section of (
|
|
131
|
+
data as { sections: { header: string; items: SubNavItem[] }[] }
|
|
132
|
+
).sections) {
|
|
133
|
+
const found = searchInItems(section.items);
|
|
134
|
+
if (found) return found;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Utility: Find top-level parent of a submenu item
|
|
141
|
+
function findTopLevelParent(
|
|
142
|
+
item: SubNavItem,
|
|
143
|
+
data: SubNavData | null
|
|
144
|
+
): SubNavItem | null {
|
|
145
|
+
if (!data || !item) return null;
|
|
146
|
+
function search(
|
|
147
|
+
items: SubNavItem[],
|
|
148
|
+
parent: SubNavItem | null
|
|
149
|
+
): SubNavItem | null {
|
|
150
|
+
for (const it of items) {
|
|
151
|
+
if (it.id === item.id) return parent ?? it;
|
|
152
|
+
if (it.children) {
|
|
153
|
+
const found = search(it.children, parent ?? it);
|
|
154
|
+
if (found) return found;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
if (Array.isArray(data)) {
|
|
160
|
+
return search(data, null);
|
|
161
|
+
} else if (
|
|
162
|
+
data &&
|
|
163
|
+
typeof data === 'object' &&
|
|
164
|
+
'sections' in data &&
|
|
165
|
+
Array.isArray(
|
|
166
|
+
(data as { sections: { header: string; items: SubNavItem[] }[] })
|
|
167
|
+
.sections
|
|
168
|
+
)
|
|
169
|
+
) {
|
|
170
|
+
for (const section of (
|
|
171
|
+
data as { sections: { header: string; items: SubNavItem[] }[] }
|
|
172
|
+
).sections) {
|
|
173
|
+
const found = search(section.items, null);
|
|
174
|
+
if (found) return found;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let currentSelectedId = $state<string | null>(null);
|
|
181
|
+
let hydrated = $state(false);
|
|
182
|
+
|
|
183
|
+
const effectiveSelectedId = $derived(() => {
|
|
184
|
+
if (!hydrated || !data) return null;
|
|
185
|
+
|
|
186
|
+
// If we already have a selectedId, use it
|
|
187
|
+
if (currentSelectedId) return currentSelectedId;
|
|
188
|
+
|
|
189
|
+
// First priority: URL hash
|
|
190
|
+
if (typeof window !== 'undefined' && window.location.hash) {
|
|
191
|
+
const hash = window.location.hash.substring(1);
|
|
192
|
+
const targetItem = findItemByHash(data, hash);
|
|
193
|
+
if (targetItem) {
|
|
194
|
+
return targetItem.id;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Second priority: sessionStorage
|
|
199
|
+
if (typeof window !== 'undefined') {
|
|
200
|
+
const storedId = sessionStorage.getItem('subNav-selectedId');
|
|
201
|
+
if (storedId) {
|
|
202
|
+
try {
|
|
203
|
+
const parsedId = JSON.parse(storedId);
|
|
204
|
+
if (parsedId && parsedId !== 'null' && parsedId !== '') {
|
|
205
|
+
const targetItem = findItemById(data, parsedId);
|
|
206
|
+
if (targetItem) {
|
|
207
|
+
return targetItem.id;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch (e) {
|
|
211
|
+
console.log('[SubNavMenu] Error parsing stored ID:', e);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Third priority: first item
|
|
217
|
+
const firstItem = findFirstItem(data);
|
|
218
|
+
if (firstItem && firstItem.title) {
|
|
219
|
+
return firstItem.id;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return null;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Update currentSelectedId when effectiveSelectedId changes
|
|
226
|
+
$effect(() => {
|
|
227
|
+
if (effectiveSelectedId() && effectiveSelectedId() !== currentSelectedId) {
|
|
228
|
+
currentSelectedId = effectiveSelectedId();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
onMount(() => {
|
|
233
|
+
hydrated = true;
|
|
234
|
+
|
|
235
|
+
if (!data) return;
|
|
236
|
+
|
|
237
|
+
// Handle header updates and scrolling
|
|
238
|
+
const targetItem = currentSelectedId
|
|
239
|
+
? findItemById(data, currentSelectedId)
|
|
240
|
+
: null;
|
|
241
|
+
if (targetItem) {
|
|
242
|
+
// Always resolve top-level parent from data
|
|
243
|
+
const topLevel = findTopLevelParent(targetItem, data) || targetItem;
|
|
244
|
+
import('@gsa-tts/graymatter-ui/stores/navigationStore')
|
|
245
|
+
.then(({ selectedSubNavItemTitle }) => {
|
|
246
|
+
selectedSubNavItemTitle.set(topLevel.title || '');
|
|
247
|
+
if (typeof window !== 'undefined') {
|
|
248
|
+
sessionStorage.setItem(
|
|
249
|
+
'subNav-selectedId',
|
|
250
|
+
JSON.stringify(targetItem.id)
|
|
251
|
+
);
|
|
252
|
+
sessionStorage.setItem(
|
|
253
|
+
'subNav-topLevelId',
|
|
254
|
+
JSON.stringify(topLevel.id)
|
|
255
|
+
);
|
|
256
|
+
sessionStorage.setItem(
|
|
257
|
+
'subNav-topLevelTitle',
|
|
258
|
+
JSON.stringify(topLevel.title || '')
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
.catch();
|
|
263
|
+
|
|
264
|
+
// Handle scrolling if we have a URL hash
|
|
265
|
+
if (targetItem.href && targetItem.href.includes('#')) {
|
|
266
|
+
const hash = targetItem.href.split('#')[1];
|
|
267
|
+
if (hash) {
|
|
268
|
+
setTimeout(() => scrollToAnchorWithOffset(hash), 100);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Always dispatch sectionVisible with top-level parent
|
|
273
|
+
if (topLevel.title && typeof window !== 'undefined') {
|
|
274
|
+
window.dispatchEvent(
|
|
275
|
+
new CustomEvent('sectionVisible', {
|
|
276
|
+
detail: {
|
|
277
|
+
title: topLevel.title,
|
|
278
|
+
id: topLevel.id,
|
|
279
|
+
},
|
|
280
|
+
})
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
subNavReady.set(true);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
function handleItemClick(event: Event, item: SubNavItem) {
|
|
288
|
+
// Always prevent default for anchor links
|
|
289
|
+
if (item.href && item.href.includes('#')) {
|
|
290
|
+
event.preventDefault();
|
|
291
|
+
} else if (!item.href || item.href === '#') {
|
|
292
|
+
event.preventDefault();
|
|
293
|
+
}
|
|
294
|
+
currentSelectedId = item.id;
|
|
295
|
+
const topLevel = findTopLevelParent(item, data) || item;
|
|
296
|
+
import('@gsa-tts/graymatter-ui/stores/navigationStore')
|
|
297
|
+
.then(({ selectedSubNavItemTitle }) => {
|
|
298
|
+
selectedSubNavItemTitle.set(topLevel.title || '');
|
|
299
|
+
if (typeof window !== 'undefined') {
|
|
300
|
+
sessionStorage.setItem('subNav-selectedId', JSON.stringify(item.id));
|
|
301
|
+
sessionStorage.setItem(
|
|
302
|
+
'subNav-topLevelId',
|
|
303
|
+
JSON.stringify(topLevel.id)
|
|
304
|
+
);
|
|
305
|
+
sessionStorage.setItem(
|
|
306
|
+
'subNav-topLevelTitle',
|
|
307
|
+
JSON.stringify(topLevel.title || '')
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
.catch();
|
|
312
|
+
if (typeof window !== 'undefined') {
|
|
313
|
+
if (item.href && item.href.includes('#')) {
|
|
314
|
+
const hash = item.href.split('#')[1];
|
|
315
|
+
if (hash) {
|
|
316
|
+
window.location.hash = hash;
|
|
317
|
+
if (window.innerWidth > 640) {
|
|
318
|
+
// Desktop: use delay for consistent timing
|
|
319
|
+
setTimeout(() => {
|
|
320
|
+
scrollToAnchorWithOffset(hash);
|
|
321
|
+
}, 350);
|
|
322
|
+
} else {
|
|
323
|
+
// Mobile: immediate scroll without delay
|
|
324
|
+
window.dispatchEvent(
|
|
325
|
+
new CustomEvent('mobileAnchorNavigate', { detail: { hash } })
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Always use top-level parent for header
|
|
331
|
+
if (topLevel.title) {
|
|
332
|
+
window.dispatchEvent(
|
|
333
|
+
new CustomEvent('sectionVisible', {
|
|
334
|
+
detail: {
|
|
335
|
+
title: topLevel.title,
|
|
336
|
+
id: topLevel.id,
|
|
337
|
+
},
|
|
338
|
+
})
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
// Mobile menu logic unchanged
|
|
342
|
+
if (window.innerWidth <= 640 && item.href && item.href !== '#') {
|
|
343
|
+
import('@gsa-tts/graymatter-ui/stores/navigationStore').then(
|
|
344
|
+
({ navigationStore }) => {
|
|
345
|
+
navigationStore.setExpanded(false);
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
if (window.innerWidth <= 640) {
|
|
350
|
+
window.dispatchEvent(new CustomEvent('closeMobileMenu'));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (item.href && item.href !== '#') {
|
|
354
|
+
dispatch('navigate', { item });
|
|
355
|
+
} else {
|
|
356
|
+
dispatch('itemSelect', { item });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function handleKeyDown(event: KeyboardEvent, item: SubNavItem) {
|
|
361
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
362
|
+
event.preventDefault();
|
|
363
|
+
handleItemClick(event, item);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Utility: Scroll to anchor with header offset
|
|
368
|
+
function scrollToAnchorWithOffset(anchorId: string) {
|
|
369
|
+
const mainContent = document.getElementById('main-content');
|
|
370
|
+
if (mainContent) {
|
|
371
|
+
const anchor = mainContent.querySelector(
|
|
372
|
+
`[id="${anchorId}"]`
|
|
373
|
+
) as HTMLElement;
|
|
374
|
+
if (anchor) {
|
|
375
|
+
// Use instant scrolling without animation
|
|
376
|
+
try {
|
|
377
|
+
// Add temporary scroll margin to account for header
|
|
378
|
+
const originalScrollMargin = anchor.style.scrollMarginTop;
|
|
379
|
+
anchor.style.scrollMarginTop = '56px';
|
|
380
|
+
|
|
381
|
+
anchor.scrollIntoView({
|
|
382
|
+
behavior: 'auto',
|
|
383
|
+
block: 'start',
|
|
384
|
+
inline: 'nearest',
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Restore original scroll margin after a delay
|
|
388
|
+
setTimeout(() => {
|
|
389
|
+
anchor.style.scrollMarginTop = originalScrollMargin;
|
|
390
|
+
}, 1000);
|
|
391
|
+
} catch {
|
|
392
|
+
// Fallback to manual calculation
|
|
393
|
+
const headerHeight = 56;
|
|
394
|
+
const rect = anchor.getBoundingClientRect();
|
|
395
|
+
const scrollTop = window.scrollY + rect.top - headerHeight;
|
|
396
|
+
const finalScrollTop = Math.max(0, scrollTop);
|
|
397
|
+
window.scrollTo({ top: finalScrollTop, behavior: 'auto' });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
</script>
|
|
403
|
+
|
|
404
|
+
<div class="sub-menu-container">
|
|
405
|
+
<div class="sub-menu-content">
|
|
406
|
+
{#if data}
|
|
407
|
+
{#if Array.isArray(data)}
|
|
408
|
+
<ApiDocSubNavList
|
|
409
|
+
items={data}
|
|
410
|
+
level={0}
|
|
411
|
+
selectedId={effectiveSelectedId()}
|
|
412
|
+
onClick={handleItemClick}
|
|
413
|
+
onKeyDown={handleKeyDown}
|
|
414
|
+
/>
|
|
415
|
+
{/if}
|
|
416
|
+
{#each sections() ?? [] as section: { header: string; items: SubNavItem[] }}
|
|
417
|
+
<div class="sub-menu-header">
|
|
418
|
+
{section.header}
|
|
419
|
+
</div>
|
|
420
|
+
<ApiDocSubNavList
|
|
421
|
+
items={section.items}
|
|
422
|
+
level={0}
|
|
423
|
+
selectedId={effectiveSelectedId()}
|
|
424
|
+
onClick={handleItemClick}
|
|
425
|
+
onKeyDown={handleKeyDown}
|
|
426
|
+
/>
|
|
427
|
+
{/each}
|
|
428
|
+
{/if}
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
<style>
|
|
433
|
+
.sub-menu-container {
|
|
434
|
+
display: flex;
|
|
435
|
+
padding: 0 var(--ai-size-8) var(--ai-size-16) var(--ai-size-8);
|
|
436
|
+
flex-direction: column;
|
|
437
|
+
align-items: flex-start;
|
|
438
|
+
gap: var(--ai-size-32);
|
|
439
|
+
align-self: stretch;
|
|
440
|
+
min-height: 6.25rem;
|
|
441
|
+
}
|
|
442
|
+
.sub-menu-content {
|
|
443
|
+
display: flex;
|
|
444
|
+
flex-direction: column;
|
|
445
|
+
align-items: flex-start;
|
|
446
|
+
gap: var(--ai-size-2);
|
|
447
|
+
align-self: stretch;
|
|
448
|
+
}
|
|
449
|
+
.sub-menu-header {
|
|
450
|
+
display: flex;
|
|
451
|
+
padding: var(--ai-size-8);
|
|
452
|
+
justify-content: flex-start;
|
|
453
|
+
align-items: center;
|
|
454
|
+
gap: var(--ai-size-10);
|
|
455
|
+
align-self: stretch;
|
|
456
|
+
color: var(--ai-color-steel-900);
|
|
457
|
+
font-size: var(--ai-size-12);
|
|
458
|
+
font-style: normal;
|
|
459
|
+
font-weight: 600;
|
|
460
|
+
line-height: 1.2;
|
|
461
|
+
letter-spacing: 0.0075rem;
|
|
462
|
+
text-transform: uppercase;
|
|
463
|
+
}
|
|
464
|
+
</style>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
const { item, level = 0, isSelected = false, onClick, onKeyDown } = $props();
|
|
3
|
+
const levelClass = $derived(() => `level-${level}`);
|
|
4
|
+
const hasHref = $derived(() => item.href && item.href !== '#');
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
{#if hasHref()}
|
|
8
|
+
<a
|
|
9
|
+
href={item.href}
|
|
10
|
+
class={`menu-item ${levelClass()}`}
|
|
11
|
+
class:selected={isSelected}
|
|
12
|
+
onclick={e => onClick(e, item)}
|
|
13
|
+
>
|
|
14
|
+
<div class="menu-item-content">
|
|
15
|
+
<span class="menu-item-text" class:selected={isSelected}>
|
|
16
|
+
{item.title}
|
|
17
|
+
</span>
|
|
18
|
+
</div>
|
|
19
|
+
</a>
|
|
20
|
+
{:else}
|
|
21
|
+
<div
|
|
22
|
+
class={`menu-item ${levelClass()}${!hasHref() ? ' non-clickable' : ''}`}
|
|
23
|
+
class:selected={isSelected}
|
|
24
|
+
tabindex="0"
|
|
25
|
+
role="button"
|
|
26
|
+
aria-label={`Select ${item.title}`}
|
|
27
|
+
onclick={e => onClick(e, item)}
|
|
28
|
+
onkeydown={e => onKeyDown(e, item)}
|
|
29
|
+
>
|
|
30
|
+
<div class="menu-item-content">
|
|
31
|
+
<span class="menu-item-text" class:selected={isSelected}>
|
|
32
|
+
{item.title}
|
|
33
|
+
</span>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
{/if}
|
|
37
|
+
|
|
38
|
+
<style>
|
|
39
|
+
.menu-item {
|
|
40
|
+
display: flex;
|
|
41
|
+
height: 2.1625rem; /* 34.6px */
|
|
42
|
+
padding: var(--ai-size-8);
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: var(--ai-size-4);
|
|
45
|
+
align-self: stretch;
|
|
46
|
+
width: 100%;
|
|
47
|
+
background: none;
|
|
48
|
+
border: none;
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
text-decoration: none;
|
|
51
|
+
color: inherit;
|
|
52
|
+
border-radius: var(--ai-size-4);
|
|
53
|
+
transition: background-color var(--ai-transition-ease-fast);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.menu-item:hover {
|
|
57
|
+
background-color: var(--ai-color-steel-200);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.menu-item:focus {
|
|
61
|
+
outline: var(--ai-size-2) solid var(--ai-color-blue-600);
|
|
62
|
+
outline-offset: var(--ai-size-2);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Hide focus outline on click */
|
|
66
|
+
.menu-item:focus:not(:focus-visible) {
|
|
67
|
+
outline: none;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.menu-item.selected {
|
|
71
|
+
background-color: var(--ai-color-steel-200);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.menu-item.non-clickable {
|
|
75
|
+
cursor: default;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.menu-item.non-clickable:hover {
|
|
79
|
+
background-color: transparent;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Nesting levels with indentation */
|
|
83
|
+
.menu-item.level-0 {
|
|
84
|
+
padding-left: var(--ai-size-8);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.menu-item.level-1 {
|
|
88
|
+
padding-left: var(--ai-size-20);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.menu-item.level-2 {
|
|
92
|
+
padding-left: var(--ai-size-32);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.menu-item-content {
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
gap: var(--ai-size-10);
|
|
99
|
+
flex: 1 0 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.menu-item-text {
|
|
103
|
+
color: var(--ai-color-steel-800);
|
|
104
|
+
font-size: var(--ai-size-14);
|
|
105
|
+
font-style: normal;
|
|
106
|
+
font-weight: 400;
|
|
107
|
+
line-height: 1.3;
|
|
108
|
+
transition: all var(--ai-transition-ease-fast);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.menu-item-text.selected,
|
|
112
|
+
.menu-item.selected .menu-item-text {
|
|
113
|
+
color: var(--ai-color-black);
|
|
114
|
+
font-weight: 600;
|
|
115
|
+
}
|
|
116
|
+
</style>
|
package/src/components/index.ts
CHANGED
|
@@ -12,6 +12,9 @@ export { default as MobileBottomNav } from './MobileBottomNav.svelte';
|
|
|
12
12
|
export { default as MobileSideMenu } from './MobileSideMenu.svelte';
|
|
13
13
|
export { default as NavigationInitializer } from './NavigationInitializer.svelte';
|
|
14
14
|
export { default as UsaSkipNav } from './UsaSkipNav.svelte';
|
|
15
|
+
export { default as ApiDocSubNavMenu } from './ApiDocSubNavMenu.svelte';
|
|
16
|
+
export { default as ApiDocSubNavList } from './ApiDocSubNavList.svelte';
|
|
17
|
+
export { default as ApiDocSubNavMenuItem } from './ApiDocSubNavMenuItem.svelte';
|
|
15
18
|
|
|
16
19
|
// Icon Components
|
|
17
20
|
export * from './icons/index.js';
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Endpoints'
|
|
3
|
+
description: ''
|
|
4
|
+
sortOrder: 20
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
API endpoints will be available when you login.
|
|
8
|
+
|
|
9
|
+
### 1. Models
|
|
10
|
+
|
|
11
|
+
GET /api/v1/models
|
|
12
|
+
|
|
13
|
+
To reach models the documented endpoint is `<base url>/api/v1/models`, which will allow you to retrieve available model information.
|
|
14
|
+
|
|
15
|
+
**Example Request**
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
curl -X 'GET' \
|
|
19
|
+
'<base url>/api/v1/models' \
|
|
20
|
+
-H 'accept: application/json' \
|
|
21
|
+
-H 'Authorization: Bearer <Your API Key>'
|
|
22
|
+
```
|
|
23
|
+
**Example Response**
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"object": "list",
|
|
28
|
+
"data": [
|
|
29
|
+
{
|
|
30
|
+
"id": "claude_3_5_sonnet",
|
|
31
|
+
"created": 1718841600,
|
|
32
|
+
"object": "model",
|
|
33
|
+
"owned_by": "Anthropic"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "llama3211b",
|
|
37
|
+
"created": 1727222400,
|
|
38
|
+
"object": "model",
|
|
39
|
+
"owned_by": "Meta"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "cohere_english_v3",
|
|
43
|
+
"created": 1698883200,
|
|
44
|
+
"object": "model",
|
|
45
|
+
"owned_by": "Cohere"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
### 2. Chat Completions
|
|
51
|
+
|
|
52
|
+
POST /api/v1/chat/completions
|
|
53
|
+
|
|
54
|
+
To reach chat completions the documented endpoint is `<base url>/api/v1/chat/completions`, which will allow you to retrieve chat completion information.
|
|
55
|
+
|
|
56
|
+
**Request Body**
|
|
57
|
+
|
|
58
|
+
* **model**: The model ID (e.g: gemini-2.0-flash, claude_3_haiku)
|
|
59
|
+
* **messages**: An array of of message items consisting of User message, Image Content, Document Content, Assistant Message
|
|
60
|
+
* **max_tokens**: Maximum response length (optional)
|
|
61
|
+
* **temperature**: Response creativity (0.0-2.0, optional)
|
|
62
|
+
|
|
63
|
+
| **Models** | **Range** | **Default value** |
|
|
64
|
+
| :------- | :------: | -------: |
|
|
65
|
+
| Google models | 0.0 - 2.0 | 1.0 |
|
|
66
|
+
| Anthropic models | 0.0 - 1.0 | 0.5 |
|
|
67
|
+
| Meta models | 0.0 - 1.0 | 0.5 |
|
|
68
|
+
|
|
69
|
+
* **top_p**: An alternative to sampling with temperature, called nucleus sampling (optional)
|
|
70
|
+
* **stream**: a boolean indicating whether to send partials responses as available (optional)
|
|
71
|
+
|
|
72
|
+
**Example Request**
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
curl -X 'POST' \
|
|
76
|
+
'<base url>/api/v1/chat/completions' \
|
|
77
|
+
-H 'accept: application/json' \
|
|
78
|
+
-H 'Authorization: Bearer <Your API Key>' \
|
|
79
|
+
-H 'Content-Type: application/json' \
|
|
80
|
+
-d '{
|
|
81
|
+
"messages": [
|
|
82
|
+
{
|
|
83
|
+
"content": "You speak only pirate",
|
|
84
|
+
"role": "system"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"content": "Hello!",
|
|
88
|
+
"role": "user"
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"model": "gemini-2.0-flash"
|
|
92
|
+
}'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Example Response**
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"object": "chat.completion",
|
|
100
|
+
"created": 1748517745,
|
|
101
|
+
"model": "gemini-2.0-flash",
|
|
102
|
+
"choices": [
|
|
103
|
+
{
|
|
104
|
+
"index": 0,
|
|
105
|
+
"message": {
|
|
106
|
+
"role": "assistant",
|
|
107
|
+
"content": "Ahoy there, matey! What brings ye to me waters?\n"
|
|
108
|
+
},
|
|
109
|
+
"finish_reason": "stop"
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"usage": {
|
|
113
|
+
"prompt_tokens": 14,
|
|
114
|
+
"completion_tokens": 15,
|
|
115
|
+
"total_tokens": 29
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 3. Embeddings
|
|
122
|
+
|
|
123
|
+
POST /api/v1/embeddings
|
|
124
|
+
|
|
125
|
+
To reach embeddings the documented endpoint is `<base url>/api/v1/embeddings`, which will allow you to retrieve embeddings information.
|
|
126
|
+
|
|
127
|
+
**Request Body**
|
|
128
|
+
|
|
129
|
+
* **model:** The model ID (e.g: cohere_english_v3)
|
|
130
|
+
* **input:** Input text to embed, encoded as a string or array of strings. Each input must not exceed the max input tokens for the model.
|
|
131
|
+
dimensions: The number of dimensions the resulting output embeddings should have. Only supported in some models (Optional)
|
|
132
|
+
* **input_type:** (Note: this is not part of the OpenAI specification but is useful on some models). Specify the kind of input to allow the model to optimize for specific uses. Options are: "search_document", "search_query", "classification", "clustering", "semantic_similarity" (Optional)
|
|
133
|
+
|
|
134
|
+
**Example Request**
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
curl -X 'POST' \
|
|
138
|
+
'<base url>/api/v1/embeddings' \
|
|
139
|
+
-H 'accept: application/json' \
|
|
140
|
+
-H 'Authorization: Bearer <Your API Key>\
|
|
141
|
+
-H 'Content-Type: application/json' \
|
|
142
|
+
-d '{
|
|
143
|
+
"encodingFormat": "float",
|
|
144
|
+
"input": "A mighty woman with a torch, whose flame / Is the imprisoned lightning",
|
|
145
|
+
"input_type": "search_document",
|
|
146
|
+
"model": "cohere_english_v3"
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
**Example Response**
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"object": "list",
|
|
154
|
+
"data": [
|
|
155
|
+
{
|
|
156
|
+
"object": "embedding",
|
|
157
|
+
"embedding": [
|
|
158
|
+
0.06933594,
|
|
159
|
+
-0.030883789,
|
|
160
|
+
0.054351807,
|
|
161
|
+
-0.0018196106,
|
|
162
|
+
…
|
|
163
|
+
0.013938904
|
|
164
|
+
],
|
|
165
|
+
"index": 0
|
|
166
|
+
}
|
|
167
|
+
],
|
|
168
|
+
"model": "cohere.embed-english-v3",
|
|
169
|
+
"usage": {
|
|
170
|
+
"promptTokens": 12,
|
|
171
|
+
"totalTokens": 12
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Getting Started'
|
|
3
|
+
description: ''
|
|
4
|
+
sortOrder: 10
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
### Introduction
|
|
8
|
+
|
|
9
|
+
USAi API provides programmatic access to AI services for government users and approved partners. It currently supports Chat Completions (model inference) and Embeddings (used for RAG and other applications).
|
|
10
|
+
|
|
11
|
+
Our API has resource-oriented URLs, accepts JSON request bodies, returns JSON responses, and uses HTTP response codes to indicate API errors, authentication messaging, and verbs.
|
|
12
|
+
|
|
13
|
+
### Models and endpoints
|
|
14
|
+
|
|
15
|
+
USAi API is organized around [REST](http://en.wikipedia.org/wiki/Representational_State_Transfer) and provides access to the large language models (LLMs):
|
|
16
|
+
|
|
17
|
+
**Google AI models**
|
|
18
|
+
- [Gemini 2.5 Flash](https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash)
|
|
19
|
+
- [Gemini 2.5 Pro](https://ai.google.dev/gemini-api/docs/models#gemini-2.5-pro)
|
|
20
|
+
|
|
21
|
+
**Anthropic models**
|
|
22
|
+
- [Claude Haiku 3.5](https://docs.claude.com/en/docs/overview)
|
|
23
|
+
- [Claude Sonnet 3.7](https://docs.claude.com/en/docs/overview)
|
|
24
|
+
- [Claude Sonnet 4](https://docs.claude.com/en/docs/overview)
|
|
25
|
+
- [Claude Opus 4](https://docs.claude.com/en/docs/overview)
|
|
26
|
+
|
|
27
|
+
**Meta models**
|
|
28
|
+
- [Llama 3.2 11B](https://www.llama.com/docs/model-cards-and-prompt-formats/llama3_2/)
|
|
29
|
+
- [Llama 4 Maverick](https://www.llama.com/docs/model-cards-and-prompt-formats/llama4/)
|
|
30
|
+
|
|
31
|
+
The interface is modeled after the [OpenAI Chat Completion API](https://platform.openai.com/docs/guides/text?api-mode=responses). We will continue to add more models over time; we may also remove models if they are found to not meet our standards. You will be notified if a model is removed.
|
|
32
|
+
|
|
33
|
+
Each model we draw on has unique capabilities. Models may also respond differently to the same API request. Below is a summary of each endpoint’s functionality:
|
|
34
|
+
|
|
35
|
+
- **Chat Completions:** The ability to send prompts and receive a response from the LLM models.
|
|
36
|
+
- **Embedding:** Converts input into numeric vectors representing semantic meaning. The vectors are used for tasks like retrieving information from documents, document analysis, and building Retrieval Augmented Generation (RAG) systems.
|
|
37
|
+
- **Models:** A list of models and IDs used to specify LLM models in requests.
|
|
38
|
+
|
|
39
|
+
#### Content types
|
|
40
|
+
|
|
41
|
+
The models USAi API draws on support multiple types of inputs; text, image, and file content types. However, each model has different input capabilities: for example, Claude Sonnet supports Optical Character Recognition (OCR) and recognizes image-only PDFs, while Claude Haiku and Llama currently do not. For additional detail about how we handle these content types, reference [OpenAI’s API documentation](https://platform.openai.com/docs/api-reference/chat/create).
|
|
42
|
+
|
|
43
|
+
### Authentication
|
|
44
|
+
|
|
45
|
+
All API requests require an API key. Upon authentication, your agency-specific instructions for requesting an API key, and the API endpoint, will be available.
|
|
46
|
+
|
|
47
|
+
### Limitations
|
|
48
|
+
|
|
49
|
+
#### Rate limitations
|
|
50
|
+
|
|
51
|
+
We currently have a rate limit of 3 calls / second / API key. If you hit a rate limit, you should expect a 429 error code. If you need additional capacity, please contact us at [support@usai.gov](mailto:support@usai.gov). We can work with our infrastructure providers to secure higher model limits.
|
|
52
|
+
|
|
53
|
+
#### Feature limitations
|
|
54
|
+
|
|
55
|
+
Because the underlying platforms have different capabilities and interfaces, not all features of the Chat Completions API are available at this time. Current known limitations include:
|
|
56
|
+
|
|
57
|
+
- Audio
|
|
58
|
+
- Structured output
|
|
59
|
+
|
|
60
|
+
#### Guardrail limitations
|
|
61
|
+
|
|
62
|
+
We do not have guardrails beyond those provided by our model providers; this means that an API user has full control over what inputs and outputs are allowed when using the API. We expect API users to interact with the API ethically and deliberately, and add their own guardrails and system prompts as needed. We recommend implementing system prompts when using the API in situations where you do not know what inputs the model will receive.
|
|
63
|
+
|
|
64
|
+
System prompts should address your needs and concerns. Some of the system prompts in USAi Chat include:
|
|
65
|
+
- You are a helpful assistant that works for a government agency.
|
|
66
|
+
- You help users with general knowledge, problem-solving, coding, and interactive tasks.
|
|
67
|
+
- You maintain a friendly, helpful, professional, and empathetic tone at all times.
|
|
68
|
+
- You want to understand the user's intent, and apply your knowledge and background to formulate the most helpful response possible.
|
|
69
|
+
- Redirect conversations that veer into inappropriate, illegal, or explicit territory.
|
|
70
|
+
- You're not an expert in government policies, security, safety, health, procurement, contracts, or law. Provide general guidance only and advise users to reference appropriate material.
|
|
71
|
+
- Prioritize historical accuracy, scientific inquiry, and objectivity in all responses.
|
|
72
|
+
- Break down complex questions and walk users through solutions step-by-step.
|
|
73
|
+
- Use real-world analogies to simplify complex concepts.
|
|
74
|
+
- When the user's request is unclear, ask for more details to help refine your response.
|
|
75
|
+
- Ask users for feedback on the answer that can help you respond more accurately.
|
|
76
|
+
- Never knowingly make false statements or deceive users.
|
|
77
|
+
- Avoid generating explicit, hateful, dangerous, or illegal content.
|
|
78
|
+
- Protect privacy and do not share personal information about individuals.
|
|
79
|
+
- Redirect users' requests around potentially controversial or polarizing topics quickly.
|
|
80
|
+
- You do not prefer or recommend specific political views, groups, religions, companies, products, or enterprise.
|
|
81
|
+
|
|
82
|
+
### Code demo
|
|
83
|
+
|
|
84
|
+
To see an example of how to implement features using Python, visit our [HTML example notebook](https://www.usai.gov/assets/files/jupyter_example.html). If you would like to run this file in a Jupyter environment, you can download the <a href="https://www.usai.gov/assets/files/jupyter_example.ipynb" download>notebook ipynb file</a>.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Support'
|
|
3
|
+
description: ''
|
|
4
|
+
sortOrder: 30
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You may encounter issues while working with the USAi API, as it is under active development. We may not be able to fix all issues immediately, but will add them to our backlog for future releases. If you have any questions, hit rate limits, or otherwise need assistance, please contact us at [support@usai.gov](mailto:support@usai.gov).
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
import GlobalAppLayout from './GlobalAppLayout.astro';
|
|
3
|
+
import ApiDocSubNavMenu from '../components/ApiDocSubNavMenu.svelte';
|
|
4
|
+
import NavigationInitializer from '../components/NavigationInitializer.svelte';
|
|
5
|
+
import { getBaseUrl } from '../helpers';
|
|
6
|
+
|
|
7
|
+
const gtmID = import.meta.env.PUBLIC_GTM_ID;
|
|
8
|
+
const {
|
|
9
|
+
navigationItem = 'api',
|
|
10
|
+
title,
|
|
11
|
+
description,
|
|
12
|
+
openGraphImage,
|
|
13
|
+
showAppIcons = false,
|
|
14
|
+
subNavData = null,
|
|
15
|
+
disableCodeCopyButtons = false,
|
|
16
|
+
postItems = null,
|
|
17
|
+
profileMenuData = null,
|
|
18
|
+
} = Astro.props;
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<GlobalAppLayout
|
|
22
|
+
ssrSelectedItem={navigationItem}
|
|
23
|
+
gtmID={gtmID}
|
|
24
|
+
{showAppIcons}
|
|
25
|
+
{profileMenuData}
|
|
26
|
+
logoLinkUrl={getBaseUrl()}
|
|
27
|
+
logoLinkLabel="Homepage"
|
|
28
|
+
title={title}
|
|
29
|
+
{description}
|
|
30
|
+
{openGraphImage}
|
|
31
|
+
>
|
|
32
|
+
<!-- Initialize navigation state and auto-expand submenu for section pages -->
|
|
33
|
+
<NavigationInitializer
|
|
34
|
+
item={navigationItem}
|
|
35
|
+
autoExpand={true}
|
|
36
|
+
client:only="svelte"
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
<!-- Documentation Sub Navigation -->
|
|
40
|
+
{
|
|
41
|
+
subNavData && (
|
|
42
|
+
<ApiDocSubNavMenu
|
|
43
|
+
data={subNavData}
|
|
44
|
+
selectedId={null}
|
|
45
|
+
slot="sub-nav"
|
|
46
|
+
client:load
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
<!-- Main content area -->
|
|
52
|
+
<div class="docs-content">
|
|
53
|
+
<article class="article-body">
|
|
54
|
+
<h1 class="page-title">API Documentation</h1>
|
|
55
|
+
|
|
56
|
+
<!-- Render postItems if provided -->
|
|
57
|
+
{
|
|
58
|
+
postItems &&
|
|
59
|
+
postItems.map((post: any) => (
|
|
60
|
+
<div>
|
|
61
|
+
<h2 class="title" id={post.id}>
|
|
62
|
+
{post.data.title}
|
|
63
|
+
</h2>
|
|
64
|
+
<post.ContentComponent />
|
|
65
|
+
</div>
|
|
66
|
+
))
|
|
67
|
+
}
|
|
68
|
+
</article>
|
|
69
|
+
</div>
|
|
70
|
+
</GlobalAppLayout>
|
|
71
|
+
|
|
72
|
+
<!-- Add copy button to all code blocks -->
|
|
73
|
+
{
|
|
74
|
+
!disableCodeCopyButtons && (
|
|
75
|
+
<script>
|
|
76
|
+
import {setupCopyButtons} from '../utils/copyButtonScript.js';
|
|
77
|
+
setupCopyButtons();
|
|
78
|
+
</script>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
<style>
|
|
83
|
+
body {
|
|
84
|
+
color: var(--ai-color-steel-900);
|
|
85
|
+
line-height: var(--ai-font-lineheight-prose);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
:is(h1, h2, h3, h4, h5, h6) {
|
|
89
|
+
color: var(--ai-color-black);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.docs-content {
|
|
93
|
+
padding: 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.article-body {
|
|
97
|
+
max-width: 84ch;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
:global(.ai-copy-btn) {
|
|
101
|
+
width: var(--ai-size-32);
|
|
102
|
+
height: var(--ai-size-32);
|
|
103
|
+
position: absolute;
|
|
104
|
+
top: var(--ai-size-8);
|
|
105
|
+
right: var(--ai-size-8);
|
|
106
|
+
background: none;
|
|
107
|
+
border-radius: var(--ai-size-4);
|
|
108
|
+
border: none;
|
|
109
|
+
padding: 0;
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
box-shadow: 0 var(--ai-size-2) var(--ai-size-8) rgba(0, 0, 0, 0.08);
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
justify-content: center;
|
|
115
|
+
transition:
|
|
116
|
+
background var(--ai-duration-fast),
|
|
117
|
+
border var(--ai-duration-fast);
|
|
118
|
+
z-index: var(--ai-layer-2);
|
|
119
|
+
overflow: hidden;
|
|
120
|
+
}
|
|
121
|
+
:global(.ai-copy-btn:hover),
|
|
122
|
+
:global(.ai-copy-btn:focus) {
|
|
123
|
+
background: var(--ai-color-steel-800);
|
|
124
|
+
}
|
|
125
|
+
:global(.ai-copy-btn svg) {
|
|
126
|
+
display: block;
|
|
127
|
+
position: relative;
|
|
128
|
+
}
|
|
129
|
+
:global(.ai-code-block-wrapper) {
|
|
130
|
+
position: relative;
|
|
131
|
+
display: block;
|
|
132
|
+
}
|
|
133
|
+
:global(pre) {
|
|
134
|
+
font-family: var(--ai-font-family-monospace);
|
|
135
|
+
font-size: var(--ai-size-13);
|
|
136
|
+
font-weight: var(--ai-font-weight-normal);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Table styling - only for documentation pages */
|
|
140
|
+
:global(.docs-content table) {
|
|
141
|
+
width: 100%;
|
|
142
|
+
border-collapse: collapse;
|
|
143
|
+
margin: var(--ai-size-24) 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
:global(.docs-content table th),
|
|
147
|
+
:global(.docs-content table td) {
|
|
148
|
+
padding: var(--ai-size-12) var(--ai-size-16);
|
|
149
|
+
text-align: left;
|
|
150
|
+
border-bottom: 1px solid var(--ai-color-neutral-200);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
:global(.docs-content table th) {
|
|
154
|
+
background-color: var(--ai-color-neutral-50);
|
|
155
|
+
font-weight: var(--ai-font-weight-semibold);
|
|
156
|
+
color: var(--ai-color-black);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
:global(.docs-content table tbody tr:hover) {
|
|
160
|
+
background-color: var(--ai-color-neutral-25);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
:global(.docs-content table tbody tr:last-child td) {
|
|
164
|
+
border-bottom: none;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@media (--ai-size-breakpoint-tablet) {
|
|
168
|
+
.docs-content {
|
|
169
|
+
padding: 2rem 0;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@media (--ai-size-breakpoint-desktop) {
|
|
174
|
+
.docs-content {
|
|
175
|
+
padding: 2rem;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface SubNavItem {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
href?: string;
|
|
5
|
+
children?: SubNavItem[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SubNavSection {
|
|
9
|
+
header: string;
|
|
10
|
+
items: SubNavItem[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SubNavData {
|
|
14
|
+
sections: SubNavSection[];
|
|
15
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
interface CopyButtonConfig {
|
|
2
|
+
buttonClass?: string;
|
|
3
|
+
copyIconClass?: string;
|
|
4
|
+
checkIconClass?: string;
|
|
5
|
+
timeout?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function initializeCopyButtons(config: CopyButtonConfig = {}) {
|
|
9
|
+
const {
|
|
10
|
+
buttonClass = 'ai-copy-btn',
|
|
11
|
+
copyIconClass = 'ai-copy-icon',
|
|
12
|
+
checkIconClass = 'ai-check-icon',
|
|
13
|
+
timeout = 1200,
|
|
14
|
+
} = config;
|
|
15
|
+
|
|
16
|
+
// Function to add a button to a visible code block
|
|
17
|
+
function addCopyButton(pre: HTMLPreElement) {
|
|
18
|
+
const codeBlock = pre.querySelector('code');
|
|
19
|
+
if (!codeBlock || pre.querySelector(`.${buttonClass}`)) return;
|
|
20
|
+
|
|
21
|
+
const wrapper = codeBlock.closest('.ai-code-block-wrapper');
|
|
22
|
+
if (!wrapper) return;
|
|
23
|
+
|
|
24
|
+
const button = document.createElement('button');
|
|
25
|
+
button.className = buttonClass;
|
|
26
|
+
button.type = 'button';
|
|
27
|
+
button.setAttribute('aria-label', 'Copy code to clipboard');
|
|
28
|
+
|
|
29
|
+
button.innerHTML = `
|
|
30
|
+
<!-- Inline Copy Icon -->
|
|
31
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
|
32
|
+
stroke-width="1.5" stroke="#f2f4f7"
|
|
33
|
+
class="usai-icon ${copyIconClass} size-6"
|
|
34
|
+
aria-hidden="true" focusable="false" role="img"
|
|
35
|
+
style="width:1.25em;height:1.25em;">
|
|
36
|
+
<path stroke-linecap="round" stroke-linejoin="round"
|
|
37
|
+
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
|
|
38
|
+
</svg>
|
|
39
|
+
|
|
40
|
+
<!-- Inline Check Icon -->
|
|
41
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
|
42
|
+
stroke-width="1.5" stroke="#f2f4f7"
|
|
43
|
+
class="usai-icon ${checkIconClass} size-6"
|
|
44
|
+
aria-hidden="true" focusable="false" role="img"
|
|
45
|
+
style="display:none; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:1.25em; height:1.25em;">
|
|
46
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
|
47
|
+
</svg>
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
button.onclick = () => {
|
|
51
|
+
const lines = codeBlock.querySelectorAll('.line');
|
|
52
|
+
const textToCopy =
|
|
53
|
+
lines.length > 0
|
|
54
|
+
? Array.from(lines)
|
|
55
|
+
.map(line => line.textContent ?? '')
|
|
56
|
+
.join('\n')
|
|
57
|
+
: (codeBlock.textContent ?? '');
|
|
58
|
+
|
|
59
|
+
navigator.clipboard.writeText(textToCopy);
|
|
60
|
+
|
|
61
|
+
const copyIcon = button.querySelector(
|
|
62
|
+
`.${copyIconClass}`
|
|
63
|
+
) as SVGElement | null;
|
|
64
|
+
const checkIcon = button.querySelector(
|
|
65
|
+
`.${checkIconClass}`
|
|
66
|
+
) as SVGElement | null;
|
|
67
|
+
|
|
68
|
+
if (copyIcon && checkIcon) {
|
|
69
|
+
copyIcon.style.display = 'none';
|
|
70
|
+
checkIcon.style.display = '';
|
|
71
|
+
button.setAttribute('aria-label', 'Copied!');
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
copyIcon.style.display = '';
|
|
74
|
+
checkIcon.style.display = 'none';
|
|
75
|
+
button.setAttribute('aria-label', 'Copy code to clipboard');
|
|
76
|
+
}, timeout);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
wrapper.appendChild(button);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const observer = new IntersectionObserver(
|
|
84
|
+
(entries, obs) => {
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
if (entry.isIntersecting) {
|
|
87
|
+
const pre = entry.target as HTMLPreElement;
|
|
88
|
+
addCopyButton(pre);
|
|
89
|
+
obs.unobserve(pre);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{ rootMargin: '200px' }
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
document.querySelectorAll('pre').forEach(pre => {
|
|
97
|
+
observer.observe(pre);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function setupCopyButtons() {
|
|
102
|
+
if (typeof window !== 'undefined') {
|
|
103
|
+
window.addEventListener('DOMContentLoaded', () => {
|
|
104
|
+
initializeCopyButtons();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { SubNavData } from '../types/subnav.js';
|
|
2
|
+
|
|
3
|
+
// Interface for API documentation metadata
|
|
4
|
+
export interface ApiDocMetadata {
|
|
5
|
+
id: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
sortOrder: number;
|
|
10
|
+
content?: string; // Raw MDX content for heading extraction
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Interface for navigation items with recursive children
|
|
14
|
+
interface NavItem {
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
href: string;
|
|
18
|
+
children: NavItem[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Extract headings from MDX content
|
|
22
|
+
function extractHeadings(content: string) {
|
|
23
|
+
const headingRegex = /^(#{1,6})\s+(.+)$/gm;
|
|
24
|
+
const headings: Array<{ level: number; text: string; id: string }> = [];
|
|
25
|
+
let match;
|
|
26
|
+
|
|
27
|
+
while ((match = headingRegex.exec(content)) !== null) {
|
|
28
|
+
const level = match[1]?.length || 0;
|
|
29
|
+
const text = match[2]?.trim() || '';
|
|
30
|
+
// Create a simple ID from the heading text
|
|
31
|
+
const id = text
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
34
|
+
.replace(/\s+/g, '-');
|
|
35
|
+
headings.push({ level, text, id });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return headings;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Convert headings to nested navigation structure
|
|
42
|
+
function headingsToNavItems(
|
|
43
|
+
headings: Array<{ level: number; text: string; id: string }>
|
|
44
|
+
): NavItem[] {
|
|
45
|
+
const items: NavItem[] = [];
|
|
46
|
+
const stack: Array<NavItem & { level: number }> = [];
|
|
47
|
+
|
|
48
|
+
headings.forEach(heading => {
|
|
49
|
+
const item = {
|
|
50
|
+
id: heading.id,
|
|
51
|
+
title: heading.text,
|
|
52
|
+
href: `#${heading.id}`,
|
|
53
|
+
children: [],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Find the correct parent level
|
|
57
|
+
while (stack.length > 0) {
|
|
58
|
+
const lastItem = stack[stack.length - 1];
|
|
59
|
+
if (lastItem && lastItem.level >= heading.level) {
|
|
60
|
+
stack.pop();
|
|
61
|
+
} else {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (stack.length === 0) {
|
|
67
|
+
// Top level item
|
|
68
|
+
items.push(item);
|
|
69
|
+
} else {
|
|
70
|
+
// Nested item
|
|
71
|
+
const parent = stack[stack.length - 1];
|
|
72
|
+
if (parent) {
|
|
73
|
+
parent.children.push(item);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
stack.push({ ...item, level: heading.level });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return items;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Generate navigation data dynamically from API documentation metadata
|
|
84
|
+
export function generateApiDocumentationNavData(
|
|
85
|
+
apiDocs: ApiDocMetadata[]
|
|
86
|
+
): SubNavData {
|
|
87
|
+
// Sort by sortOrder
|
|
88
|
+
const sortedDocs = [...apiDocs].sort((a, b) => a.sortOrder - b.sortOrder);
|
|
89
|
+
|
|
90
|
+
// Convert to navigation items with dynamic heading extraction
|
|
91
|
+
const items = sortedDocs.map(doc => {
|
|
92
|
+
// Extract headings from content if provided
|
|
93
|
+
const children = doc.content
|
|
94
|
+
? headingsToNavItems(extractHeadings(doc.content))
|
|
95
|
+
: [];
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
id: doc.slug,
|
|
99
|
+
title: doc.title,
|
|
100
|
+
href: `/api/documentation#${doc.slug}`,
|
|
101
|
+
children,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
sections: [
|
|
107
|
+
{
|
|
108
|
+
header: 'API Documentation',
|
|
109
|
+
items,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
}
|
package/src/utils/index.ts
CHANGED