@brainfish-ai/devdoc 0.1.43 → 0.1.45
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/cli/commands/create.js +2 -2
- package/package.json +1 -1
- package/renderer/app/api/collections/route.js +35 -4
- package/renderer/app/api/docs/route.js +9 -4
- package/renderer/app/api/suggestions/route.js +33 -13
- package/renderer/app/globals.css +69 -0
- package/renderer/app/layout.js +2 -2
- package/renderer/app/llms-full.txt/route.js +10 -1
- package/renderer/app/llms.txt/route.js +10 -1
- package/renderer/app/sitemap.xml/route.js +11 -1
- package/renderer/components/docs/mdx/cards.js +1 -1
- package/renderer/components/docs/mdx/landing.js +7 -5
- package/renderer/components/docs-viewer/agent/agent-chat.js +13 -112
- package/renderer/components/docs-viewer/agent/agent-popup-button.js +99 -0
- package/renderer/components/docs-viewer/agent/index.js +3 -0
- package/renderer/components/docs-viewer/content/content-router.js +182 -0
- package/renderer/components/docs-viewer/content/doc-page.js +73 -37
- package/renderer/components/docs-viewer/content/index.js +2 -0
- package/renderer/components/docs-viewer/content/mdx-error-boundary.js +184 -0
- package/renderer/components/docs-viewer/index.js +381 -485
- package/renderer/components/docs-viewer/playground/graphql-playground.js +205 -3
- package/renderer/components/docs-viewer/sidebar/right-sidebar.js +35 -39
- package/renderer/components/theme-toggle.js +1 -21
- package/renderer/hooks/use-route-state.js +159 -0
- package/renderer/lib/api-docs/agent/use-suggestions.js +97 -0
- package/renderer/lib/api-docs/code-editor/mode-context.js +61 -89
- package/renderer/lib/api-docs/mobile-context.js +40 -3
- package/renderer/lib/docs/config/environment.js +38 -0
- package/renderer/lib/docs/config/index.js +1 -0
- package/renderer/lib/docs/config/schema.js +17 -5
- package/renderer/lib/docs/mdx/compiler.js +5 -2
- package/renderer/lib/docs/mdx/index.js +2 -0
- package/renderer/lib/docs/mdx/remark-mermaid.js +63 -0
- package/renderer/lib/docs/navigation/index.js +1 -2
- package/renderer/lib/docs/navigation/types.js +3 -1
- package/renderer/lib/docs-navigation.js +140 -0
- package/renderer/package.json +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useState, useCallback, useRef } from 'react';
|
|
3
|
+
import { useEffect, useState, useCallback, useRef, useMemo } from 'react';
|
|
4
4
|
import { Spinner } from '@phosphor-icons/react';
|
|
5
5
|
import { DocsSidebar } from './sidebar';
|
|
6
6
|
import { NotFoundPage } from './content/not-found-page';
|
|
@@ -11,7 +11,7 @@ import { DocPage } from './content/doc-page';
|
|
|
11
11
|
import { ChangelogPage } from './content/changelog-page';
|
|
12
12
|
import { GraphQLPlayground } from './playground/graphql-playground';
|
|
13
13
|
import { makeBrainfishCollection } from '@/lib/api-docs/factories';
|
|
14
|
-
import {
|
|
14
|
+
import { buildSchema, isNonNullType, isListType } from 'graphql';
|
|
15
15
|
import { GlobalAuthModal } from './global-auth-modal';
|
|
16
16
|
import { AuthProvider } from '@/lib/api-docs/auth';
|
|
17
17
|
import { PlaygroundProvider, usePlaygroundPrefill } from '@/lib/api-docs/playground/context';
|
|
@@ -27,57 +27,59 @@ import { Code, TestTube, Book } from '@phosphor-icons/react';
|
|
|
27
27
|
import { cn } from '@/lib/utils';
|
|
28
28
|
import { SearchDialog, useSearch } from './search';
|
|
29
29
|
import { useTheme } from 'next-themes';
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
default:
|
|
40
|
-
return 'Unknown';
|
|
30
|
+
import { useRouteState } from '@/hooks/use-route-state';
|
|
31
|
+
import { navigateToPage, navigateToEndpoint, navigateToSection, navigateToTab } from '@/lib/docs-navigation';
|
|
32
|
+
import { useMobile } from '@/lib/api-docs/mobile-context';
|
|
33
|
+
import { AgentPopupButton } from './agent/agent-popup-button';
|
|
34
|
+
import { buildEndpointIndex } from '@/lib/api-docs/agent/indexer';
|
|
35
|
+
// Helper to convert GraphQL type to string representation
|
|
36
|
+
function graphqlTypeToString(type) {
|
|
37
|
+
if (isNonNullType(type)) {
|
|
38
|
+
return `${graphqlTypeToString(type.ofType)}!`;
|
|
41
39
|
}
|
|
40
|
+
if (isListType(type)) {
|
|
41
|
+
return `[${graphqlTypeToString(type.ofType)}]`;
|
|
42
|
+
}
|
|
43
|
+
// Named type
|
|
44
|
+
return type.name || 'Unknown';
|
|
42
45
|
}
|
|
43
|
-
// Parse GraphQL schema using
|
|
46
|
+
// Parse GraphQL schema using buildSchema (for SDL)
|
|
44
47
|
function parseGraphQLSchema(schemaSDL) {
|
|
45
48
|
const operations = [];
|
|
46
49
|
try {
|
|
47
|
-
const
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
50
|
+
const schema = buildSchema(schemaSDL);
|
|
51
|
+
// Get root operation types
|
|
52
|
+
const rootTypes = [
|
|
53
|
+
{
|
|
54
|
+
type: schema.getQueryType(),
|
|
55
|
+
operationType: 'query'
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: schema.getMutationType(),
|
|
59
|
+
operationType: 'mutation'
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: schema.getSubscriptionType(),
|
|
63
|
+
operationType: 'subscription'
|
|
64
|
+
}
|
|
65
|
+
];
|
|
66
|
+
for (const { type: rootType, operationType } of rootTypes){
|
|
67
|
+
if (!rootType) continue;
|
|
68
|
+
const fields = rootType.getFields();
|
|
69
|
+
for (const [fieldName, field] of Object.entries(fields)){
|
|
70
|
+
// Skip internal fields
|
|
71
|
+
if (fieldName.startsWith('_')) continue;
|
|
72
|
+
const returnType = graphqlTypeToString(field.type);
|
|
73
|
+
const query = generateGraphQLQueryFromField(operationType, fieldName, field.args, returnType, schema);
|
|
74
|
+
const exampleVariables = generateGraphQLVariablesFromArgs(field.args, schema);
|
|
75
|
+
operations.push({
|
|
76
|
+
id: `${operationType}-${fieldName}`,
|
|
77
|
+
name: fieldName,
|
|
78
|
+
description: field.description || null,
|
|
79
|
+
operationType,
|
|
80
|
+
query,
|
|
81
|
+
exampleVariables
|
|
82
|
+
});
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
} catch (err) {
|
|
@@ -85,16 +87,59 @@ function parseGraphQLSchema(schemaSDL) {
|
|
|
85
87
|
}
|
|
86
88
|
return operations;
|
|
87
89
|
}
|
|
88
|
-
//
|
|
89
|
-
function
|
|
90
|
+
// Get default fields for a type from schema
|
|
91
|
+
function getDefaultFieldsForType(typeName, schema) {
|
|
92
|
+
const type = schema.getType(typeName);
|
|
93
|
+
if (!type || !('getFields' in type)) return [];
|
|
94
|
+
const fields = type.getFields();
|
|
95
|
+
const fieldNames = Object.keys(fields);
|
|
96
|
+
// Priority order for default fields
|
|
97
|
+
const priorityFields = [
|
|
98
|
+
'code',
|
|
99
|
+
'id',
|
|
100
|
+
'name',
|
|
101
|
+
'title',
|
|
102
|
+
'slug'
|
|
103
|
+
];
|
|
104
|
+
const selectedFields = [];
|
|
105
|
+
// First, add priority fields if they exist
|
|
106
|
+
for (const pf of priorityFields){
|
|
107
|
+
if (fieldNames.includes(pf)) {
|
|
108
|
+
selectedFields.push(pf);
|
|
109
|
+
if (selectedFields.length >= 3) break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// If we don't have enough, add other scalar fields
|
|
113
|
+
if (selectedFields.length < 3) {
|
|
114
|
+
for (const [fname, fdef] of Object.entries(fields)){
|
|
115
|
+
if (selectedFields.includes(fname)) continue;
|
|
116
|
+
const ftype = graphqlTypeToString(fdef.type).replace(/[\[\]!]/g, '').trim();
|
|
117
|
+
if ([
|
|
118
|
+
'String',
|
|
119
|
+
'Int',
|
|
120
|
+
'Float',
|
|
121
|
+
'Boolean',
|
|
122
|
+
'ID'
|
|
123
|
+
].includes(ftype)) {
|
|
124
|
+
selectedFields.push(fname);
|
|
125
|
+
if (selectedFields.length >= 3) break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return selectedFields.length > 0 ? selectedFields : [
|
|
130
|
+
'__typename'
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
// Generate example GraphQL query from schema field
|
|
134
|
+
function generateGraphQLQueryFromField(operationType, name, args, returnType, schema) {
|
|
90
135
|
let query = `${operationType} ${name.charAt(0).toUpperCase() + name.slice(1)}`;
|
|
91
136
|
if (args.length > 0) {
|
|
92
|
-
const varDefs = args.map((arg)=>`$${arg.name
|
|
137
|
+
const varDefs = args.map((arg)=>`$${arg.name}: ${graphqlTypeToString(arg.type)}`).join(', ');
|
|
93
138
|
query += `(${varDefs})`;
|
|
94
139
|
}
|
|
95
140
|
query += ` {\n ${name}`;
|
|
96
141
|
if (args.length > 0) {
|
|
97
|
-
const argPairs = args.map((arg)=>`${arg.name
|
|
142
|
+
const argPairs = args.map((arg)=>`${arg.name}: $${arg.name}`).join(', ');
|
|
98
143
|
query += `(${argPairs})`;
|
|
99
144
|
}
|
|
100
145
|
const baseType = returnType.replace(/[\[\]!]/g, '').trim();
|
|
@@ -107,34 +152,126 @@ function generateGraphQLQuery(operationType, name, args, returnType) {
|
|
|
107
152
|
].includes(baseType)) {
|
|
108
153
|
query += '\n}';
|
|
109
154
|
} else {
|
|
110
|
-
|
|
155
|
+
// Get actual fields from the schema for this type
|
|
156
|
+
const defaultFields = getDefaultFieldsForType(baseType, schema);
|
|
157
|
+
const fieldsStr = defaultFields.map((f)=>` ${f}`).join('\n');
|
|
158
|
+
query += ` {\n${fieldsStr}\n }\n}`;
|
|
111
159
|
}
|
|
112
160
|
return query;
|
|
113
161
|
}
|
|
114
|
-
// Generate example
|
|
115
|
-
function
|
|
162
|
+
// Generate example value for a GraphQL type
|
|
163
|
+
function generateExampleValue(type, schema, depth = 0) {
|
|
164
|
+
// Prevent infinite recursion
|
|
165
|
+
if (depth > 3) return null;
|
|
166
|
+
// Unwrap NonNull
|
|
167
|
+
if (isNonNullType(type)) {
|
|
168
|
+
return generateExampleValue(type.ofType, schema, depth);
|
|
169
|
+
}
|
|
170
|
+
// Handle List types - return array with one example element
|
|
171
|
+
if (isListType(type)) {
|
|
172
|
+
const itemValue = generateExampleValue(type.ofType, schema, depth);
|
|
173
|
+
return itemValue !== null ? [
|
|
174
|
+
itemValue
|
|
175
|
+
] : [];
|
|
176
|
+
}
|
|
177
|
+
// Get the named type
|
|
178
|
+
const typeName = type.name;
|
|
179
|
+
if (!typeName) return null;
|
|
180
|
+
// Built-in scalar types
|
|
181
|
+
switch(typeName){
|
|
182
|
+
case 'String':
|
|
183
|
+
return 'example';
|
|
184
|
+
case 'Int':
|
|
185
|
+
return 10;
|
|
186
|
+
case 'Float':
|
|
187
|
+
return 1.5;
|
|
188
|
+
case 'Boolean':
|
|
189
|
+
return true;
|
|
190
|
+
case 'ID':
|
|
191
|
+
return 'example-id';
|
|
192
|
+
// Common custom scalars
|
|
193
|
+
case 'DateTime':
|
|
194
|
+
return new Date().toISOString();
|
|
195
|
+
case 'Date':
|
|
196
|
+
return new Date().toISOString().split('T')[0];
|
|
197
|
+
case 'Time':
|
|
198
|
+
return '12:00:00';
|
|
199
|
+
case 'JSON':
|
|
200
|
+
return {};
|
|
201
|
+
case 'JSONObject':
|
|
202
|
+
return {};
|
|
203
|
+
case 'URL':
|
|
204
|
+
return 'https://example.com';
|
|
205
|
+
case 'URI':
|
|
206
|
+
return 'https://example.com';
|
|
207
|
+
case 'Email':
|
|
208
|
+
return 'user@example.com';
|
|
209
|
+
case 'EmailAddress':
|
|
210
|
+
return 'user@example.com';
|
|
211
|
+
case 'UUID':
|
|
212
|
+
return '550e8400-e29b-41d4-a716-446655440000';
|
|
213
|
+
case 'BigInt':
|
|
214
|
+
return '9007199254740991';
|
|
215
|
+
case 'Long':
|
|
216
|
+
return 9007199254740991;
|
|
217
|
+
}
|
|
218
|
+
// Check if it's an enum type
|
|
219
|
+
const schemaType = schema.getType(typeName);
|
|
220
|
+
if (schemaType && 'getValues' in schemaType) {
|
|
221
|
+
const enumType = schemaType;
|
|
222
|
+
const values = enumType.getValues();
|
|
223
|
+
if (values.length > 0) {
|
|
224
|
+
return values[0].name // Return first enum value
|
|
225
|
+
;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Check if it's an input object type
|
|
229
|
+
if (schemaType && 'getFields' in schemaType) {
|
|
230
|
+
const inputType = schemaType;
|
|
231
|
+
const fields = inputType.getFields();
|
|
232
|
+
const result = {};
|
|
233
|
+
// Only include required fields to keep examples concise
|
|
234
|
+
for (const [fieldName, field] of Object.entries(fields)){
|
|
235
|
+
if (isNonNullType(field.type)) {
|
|
236
|
+
// Use default value if available, otherwise generate
|
|
237
|
+
if (field.defaultValue !== undefined) {
|
|
238
|
+
result[fieldName] = field.defaultValue;
|
|
239
|
+
} else {
|
|
240
|
+
result[fieldName] = generateExampleValue(field.type, schema, depth + 1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// If no required fields, include first optional field
|
|
245
|
+
if (Object.keys(result).length === 0) {
|
|
246
|
+
const firstField = Object.entries(fields)[0];
|
|
247
|
+
if (firstField) {
|
|
248
|
+
const [fname, fdef] = firstField;
|
|
249
|
+
// Use default value if available
|
|
250
|
+
if (fdef.defaultValue !== undefined) {
|
|
251
|
+
result[fname] = fdef.defaultValue;
|
|
252
|
+
} else {
|
|
253
|
+
result[fname] = generateExampleValue(fdef.type, schema, depth + 1);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
// Unknown type - return null
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
// Generate example variables from GraphQL arguments
|
|
263
|
+
function generateGraphQLVariablesFromArgs(args, schema) {
|
|
116
264
|
const variables = {};
|
|
117
265
|
for (const arg of args){
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
variables[name] =
|
|
126
|
-
|
|
127
|
-
case 'Float':
|
|
128
|
-
variables[name] = 1.0;
|
|
129
|
-
break;
|
|
130
|
-
case 'Boolean':
|
|
131
|
-
variables[name] = true;
|
|
132
|
-
break;
|
|
133
|
-
case 'ID':
|
|
134
|
-
variables[name] = '1';
|
|
135
|
-
break;
|
|
136
|
-
default:
|
|
137
|
-
variables[name] = {};
|
|
266
|
+
// First, check if the argument has a default value in the schema
|
|
267
|
+
if (arg.defaultValue !== undefined) {
|
|
268
|
+
variables[arg.name] = arg.defaultValue;
|
|
269
|
+
} else {
|
|
270
|
+
// Generate example value for arguments without defaults
|
|
271
|
+
const value = generateExampleValue(arg.type, schema, 0);
|
|
272
|
+
if (value !== null) {
|
|
273
|
+
variables[arg.name] = value;
|
|
274
|
+
}
|
|
138
275
|
}
|
|
139
276
|
}
|
|
140
277
|
return variables;
|
|
@@ -271,18 +408,45 @@ function getFirstDocPageForTab(docGroups, tabId) {
|
|
|
271
408
|
}
|
|
272
409
|
function DocsContent() {
|
|
273
410
|
const [collection, setCollection] = useState(null);
|
|
274
|
-
const [selectedRequest, setSelectedRequest] = useState(null);
|
|
275
|
-
const [selectedDocSection, setSelectedDocSection] = useState(null);
|
|
276
|
-
const [selectedDocPage, setSelectedDocPage] = useState(null);
|
|
277
|
-
const [activeTab, setActiveTab] = useState('api-reference');
|
|
278
411
|
const [selectedApiVersion, setSelectedApiVersion] = useState(null);
|
|
279
412
|
const [loading, setLoading] = useState(true);
|
|
280
413
|
const [isVersionLoading, setIsVersionLoading] = useState(false) // For version switch only
|
|
281
414
|
;
|
|
282
415
|
const [error, setError] = useState(null);
|
|
283
416
|
const [showAuthModal, setShowAuthModal] = useState(false);
|
|
284
|
-
|
|
285
|
-
;
|
|
417
|
+
// URL-based route state - single source of truth for selection
|
|
418
|
+
const routeState = useRouteState();
|
|
419
|
+
// Derive active tab from URL (with default fallback)
|
|
420
|
+
const activeTab = routeState.tab || 'api-reference';
|
|
421
|
+
// Derive selected doc page from URL
|
|
422
|
+
const selectedDocPage = routeState.contentType === 'page' ? routeState.contentId : null;
|
|
423
|
+
// Derive selected doc section from URL
|
|
424
|
+
const selectedDocSection = routeState.contentType === 'section' ? routeState.contentId : null;
|
|
425
|
+
// Derive selected request from URL by looking up in collection
|
|
426
|
+
const selectedRequest = useMemo(()=>{
|
|
427
|
+
if (routeState.contentType !== 'endpoint' || !routeState.contentId || !collection) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
return findRequestById(collection, routeState.contentId);
|
|
431
|
+
}, [
|
|
432
|
+
routeState.contentType,
|
|
433
|
+
routeState.contentId,
|
|
434
|
+
collection
|
|
435
|
+
]);
|
|
436
|
+
// Derive not found slug from URL when endpoint doesn't exist
|
|
437
|
+
const notFoundSlug = useMemo(()=>{
|
|
438
|
+
if (routeState.contentType === 'endpoint' && routeState.contentId && collection) {
|
|
439
|
+
const request = findRequestById(collection, routeState.contentId);
|
|
440
|
+
if (!request) {
|
|
441
|
+
return `endpoint/${routeState.contentId}`;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return null;
|
|
445
|
+
}, [
|
|
446
|
+
routeState.contentType,
|
|
447
|
+
routeState.contentId,
|
|
448
|
+
collection
|
|
449
|
+
]);
|
|
286
450
|
// Prefill context for agent
|
|
287
451
|
const { setPrefill } = usePlaygroundPrefill();
|
|
288
452
|
// Playground navigation context for tab navigation
|
|
@@ -291,146 +455,36 @@ function DocsContent() {
|
|
|
291
455
|
const { switchToDocs } = useModeContext();
|
|
292
456
|
// Ref for the scrollable content area
|
|
293
457
|
const contentRef = useRef(null);
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const currentTab = tab || activeTab;
|
|
298
|
-
let newUrl;
|
|
299
|
-
if (!hash) {
|
|
300
|
-
// Just the tab
|
|
301
|
-
newUrl = `#${currentTab}`;
|
|
302
|
-
} else {
|
|
303
|
-
// Tab + path
|
|
304
|
-
newUrl = `#${currentTab}/${hash}`;
|
|
305
|
-
}
|
|
306
|
-
window.history.pushState(null, '', newUrl);
|
|
307
|
-
}, [
|
|
308
|
-
activeTab
|
|
309
|
-
]);
|
|
310
|
-
// Navigate to hash - used for initial load and popstate
|
|
311
|
-
// Parse hash format: #tab or #tab/type/id
|
|
312
|
-
const parseHash = useCallback((hash)=>{
|
|
313
|
-
if (!hash) return {
|
|
314
|
-
tab: null,
|
|
315
|
-
type: null,
|
|
316
|
-
id: null
|
|
317
|
-
};
|
|
318
|
-
const parts = hash.split('/');
|
|
319
|
-
const tab = parts[0] || null;
|
|
320
|
-
const type = parts[1] || null;
|
|
321
|
-
const id = parts.slice(2).join('/') || null // Rejoin in case id has slashes
|
|
322
|
-
;
|
|
323
|
-
return {
|
|
324
|
-
tab,
|
|
325
|
-
type,
|
|
326
|
-
id
|
|
327
|
-
};
|
|
328
|
-
}, []);
|
|
329
|
-
const navigateToHash = useCallback((collectionData)=>{
|
|
330
|
-
const hash = window.location.hash.slice(1) // Remove #
|
|
331
|
-
;
|
|
332
|
-
if (!hash) {
|
|
333
|
-
// No hash - auto-select first endpoint
|
|
334
|
-
setSelectedDocSection(null);
|
|
335
|
-
setSelectedDocPage(null);
|
|
336
|
-
const firstEndpoint = getFirstEndpoint(collectionData);
|
|
337
|
-
if (firstEndpoint) {
|
|
338
|
-
setSelectedRequest(firstEndpoint);
|
|
339
|
-
} else {
|
|
340
|
-
setSelectedRequest(null);
|
|
341
|
-
}
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
// Notes mode is handled by ModeContext, just clear API selection
|
|
345
|
-
if (hash === 'notes' || hash.startsWith('notes/')) {
|
|
346
|
-
setSelectedRequest(null);
|
|
347
|
-
setSelectedDocSection(null);
|
|
348
|
-
setSelectedDocPage(null);
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
// Parse new format: #tab/type/id
|
|
352
|
-
const { tab, type, id } = parseHash(hash);
|
|
353
|
-
// Set the active tab if specified
|
|
354
|
-
if (tab) {
|
|
355
|
-
setActiveTab(tab);
|
|
356
|
-
}
|
|
357
|
-
// Handle legacy format (endpoint/xxx, page/xxx, doc/xxx without tab prefix)
|
|
358
|
-
const legacyType = hash.startsWith('endpoint/') ? 'endpoint' : hash.startsWith('page/') ? 'page' : hash.startsWith('doc/') ? 'doc' : null;
|
|
359
|
-
const actualType = type || legacyType;
|
|
360
|
-
const actualId = id || (legacyType ? hash.replace(`${legacyType}/`, '') : null);
|
|
361
|
-
if (actualType === 'endpoint' && actualId) {
|
|
362
|
-
const request = findRequestById(collectionData, actualId);
|
|
363
|
-
if (request) {
|
|
364
|
-
setSelectedRequest(request);
|
|
365
|
-
setSelectedDocSection(null);
|
|
366
|
-
setSelectedDocPage(null);
|
|
367
|
-
setNotFoundSlug(null);
|
|
368
|
-
} else {
|
|
369
|
-
// Endpoint not found - show 404 page
|
|
370
|
-
setSelectedRequest(null);
|
|
371
|
-
setSelectedDocSection(null);
|
|
372
|
-
setSelectedDocPage(null);
|
|
373
|
-
setNotFoundSlug(`endpoint/${actualId}`);
|
|
374
|
-
}
|
|
375
|
-
} else if (actualType === 'page' && actualId) {
|
|
376
|
-
setNotFoundSlug(null); // Clear not found state - DocPage handles its own 404
|
|
377
|
-
setSelectedDocPage(actualId);
|
|
378
|
-
setSelectedRequest(null);
|
|
379
|
-
setSelectedDocSection(null);
|
|
380
|
-
} else if (actualType === 'doc' && actualId) {
|
|
381
|
-
setSelectedDocSection(actualId);
|
|
382
|
-
setSelectedRequest(null);
|
|
383
|
-
setSelectedDocPage(null);
|
|
384
|
-
// Scroll to section after DOM update
|
|
385
|
-
setTimeout(()=>{
|
|
386
|
-
const element = document.getElementById(actualId);
|
|
387
|
-
if (element) {
|
|
388
|
-
element.scrollIntoView({
|
|
389
|
-
behavior: 'smooth',
|
|
390
|
-
block: 'start'
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
}, 100);
|
|
394
|
-
} else if (tab && !type) {
|
|
395
|
-
// Just a tab, no specific content - show default for that tab
|
|
396
|
-
setSelectedRequest(null);
|
|
397
|
-
setSelectedDocSection(null);
|
|
398
|
-
setSelectedDocPage(null);
|
|
399
|
-
}
|
|
400
|
-
}, [
|
|
401
|
-
parseHash
|
|
402
|
-
]);
|
|
458
|
+
// Track if initial navigation has been done (prevents re-navigation on effect re-runs)
|
|
459
|
+
const hasInitialNavigated = useRef(false);
|
|
460
|
+
// Handle selecting a request - just navigates to URL
|
|
403
461
|
const handleSelectRequest = useCallback((request)=>{
|
|
404
|
-
|
|
405
|
-
setSelectedDocSection(null);
|
|
406
|
-
setSelectedDocPage(null);
|
|
407
|
-
setNotFoundSlug(null); // Clear 404 state when selecting an endpoint
|
|
408
|
-
updateUrlHash(`endpoint/${request.id}`);
|
|
462
|
+
navigateToEndpoint(activeTab, request.id);
|
|
409
463
|
// Reset tab navigation so the new endpoint can determine its default tab
|
|
410
464
|
resetNavigation();
|
|
411
465
|
// Switch to Docs mode to show endpoint documentation first
|
|
412
466
|
switchToDocs();
|
|
413
467
|
}, [
|
|
414
|
-
|
|
468
|
+
activeTab,
|
|
415
469
|
resetNavigation,
|
|
416
470
|
switchToDocs
|
|
417
471
|
]);
|
|
472
|
+
// Handle selecting a documentation section (scroll to heading)
|
|
418
473
|
const handleSelectDocumentation = useCallback((headingId)=>{
|
|
419
474
|
const isIntro = headingId === 'introduction';
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
updateUrlHash(isIntro ? '' : `doc/${headingId}`);
|
|
425
|
-
// Switch to Docs mode when selecting documentation
|
|
426
|
-
switchToDocs();
|
|
427
|
-
setTimeout(()=>{
|
|
428
|
-
if (isIntro) {
|
|
475
|
+
if (isIntro) {
|
|
476
|
+
// Scroll to top
|
|
477
|
+
navigateToTab(activeTab);
|
|
478
|
+
setTimeout(()=>{
|
|
429
479
|
contentRef.current?.scrollTo({
|
|
430
480
|
top: 0,
|
|
431
481
|
behavior: 'smooth'
|
|
432
482
|
});
|
|
433
|
-
}
|
|
483
|
+
}, 50);
|
|
484
|
+
} else {
|
|
485
|
+
// Navigate to section and scroll
|
|
486
|
+
navigateToSection(activeTab, headingId);
|
|
487
|
+
setTimeout(()=>{
|
|
434
488
|
const element = document.getElementById(headingId);
|
|
435
489
|
if (element) {
|
|
436
490
|
element.scrollIntoView({
|
|
@@ -438,12 +492,15 @@ function DocsContent() {
|
|
|
438
492
|
block: 'start'
|
|
439
493
|
});
|
|
440
494
|
}
|
|
441
|
-
}
|
|
442
|
-
}
|
|
495
|
+
}, 50);
|
|
496
|
+
}
|
|
497
|
+
// Switch to Docs mode when selecting documentation
|
|
498
|
+
switchToDocs();
|
|
443
499
|
}, [
|
|
444
|
-
|
|
500
|
+
activeTab,
|
|
445
501
|
switchToDocs
|
|
446
502
|
]);
|
|
503
|
+
// Handle selecting a doc page
|
|
447
504
|
const handleSelectDocPage = useCallback((slug)=>{
|
|
448
505
|
// Find which tab this doc page belongs to
|
|
449
506
|
let targetTab = activeTab;
|
|
@@ -488,13 +545,8 @@ function DocsContent() {
|
|
|
488
545
|
}
|
|
489
546
|
}
|
|
490
547
|
}
|
|
491
|
-
//
|
|
492
|
-
|
|
493
|
-
setSelectedDocPage(pageSlug);
|
|
494
|
-
setSelectedRequest(null);
|
|
495
|
-
setSelectedDocSection(null);
|
|
496
|
-
setNotFoundSlug(null); // Clear 404 state
|
|
497
|
-
updateUrlHash(`page/${pageSlug}`, targetTab);
|
|
548
|
+
// Navigate to the page via URL
|
|
549
|
+
navigateToPage(targetTab, pageSlug);
|
|
498
550
|
switchToDocs();
|
|
499
551
|
// Scroll to specific release if navigating to a changelog entry
|
|
500
552
|
if (releaseSlug) {
|
|
@@ -529,7 +581,6 @@ function DocsContent() {
|
|
|
529
581
|
}, 50);
|
|
530
582
|
}
|
|
531
583
|
}, [
|
|
532
|
-
updateUrlHash,
|
|
533
584
|
switchToDocs,
|
|
534
585
|
collection,
|
|
535
586
|
activeTab
|
|
@@ -538,16 +589,15 @@ function DocsContent() {
|
|
|
538
589
|
const handleApiVersionChange = useCallback((version)=>{
|
|
539
590
|
if (version !== selectedApiVersion) {
|
|
540
591
|
setSelectedApiVersion(version);
|
|
541
|
-
//
|
|
542
|
-
|
|
543
|
-
setSelectedDocSection(null);
|
|
592
|
+
// Navigate to tab only (clears selection) when switching versions
|
|
593
|
+
navigateToTab(activeTab);
|
|
544
594
|
}
|
|
545
595
|
}, [
|
|
546
|
-
selectedApiVersion
|
|
596
|
+
selectedApiVersion,
|
|
597
|
+
activeTab
|
|
547
598
|
]);
|
|
548
599
|
// Handle tab change from header
|
|
549
600
|
const handleTabChange = useCallback((tabId)=>{
|
|
550
|
-
setActiveTab(tabId);
|
|
551
601
|
// Reset mode to docs when switching tabs (exit sandbox/api-client mode)
|
|
552
602
|
switchToDocs();
|
|
553
603
|
// Find the tab config to check its type
|
|
@@ -555,68 +605,51 @@ function DocsContent() {
|
|
|
555
605
|
const isApiTab = tabConfig?.type === 'openapi' || tabConfig?.type === 'graphql' || tabId === 'api-reference';
|
|
556
606
|
if (tabId === 'changelog') {
|
|
557
607
|
// Switch to Changelog tab
|
|
558
|
-
|
|
559
|
-
setSelectedRequest(null);
|
|
560
|
-
setSelectedDocSection(null);
|
|
561
|
-
updateUrlHash('', tabId);
|
|
608
|
+
navigateToTab(tabId);
|
|
562
609
|
} else if (isApiTab) {
|
|
563
610
|
// API Reference or GraphQL tab
|
|
564
|
-
setSelectedDocSection(null);
|
|
565
611
|
// Check if there are doc groups for this tab (MDX pages)
|
|
566
612
|
const hasGroups = hasDocGroupsForTab(collection?.docGroups, tabId);
|
|
567
613
|
if (hasGroups) {
|
|
568
614
|
// Has doc groups - select the first page from groups
|
|
569
615
|
const firstPage = getFirstDocPageForTab(collection?.docGroups, tabId);
|
|
570
616
|
if (firstPage) {
|
|
571
|
-
|
|
572
|
-
setSelectedRequest(null);
|
|
573
|
-
updateUrlHash(`page/${firstPage}`, tabId);
|
|
617
|
+
navigateToPage(tabId, firstPage);
|
|
574
618
|
switchToDocs();
|
|
575
619
|
} else {
|
|
576
620
|
// No pages in groups - auto-select first endpoint
|
|
577
|
-
setSelectedDocPage(null);
|
|
578
621
|
const firstEndpoint = collection ? getFirstEndpoint(collection) : null;
|
|
579
622
|
if (firstEndpoint) {
|
|
580
|
-
|
|
581
|
-
updateUrlHash(`endpoint/${firstEndpoint.id}`, tabId);
|
|
623
|
+
navigateToEndpoint(tabId, firstEndpoint.id);
|
|
582
624
|
} else {
|
|
583
|
-
|
|
584
|
-
updateUrlHash('', tabId);
|
|
625
|
+
navigateToTab(tabId);
|
|
585
626
|
}
|
|
586
627
|
}
|
|
587
628
|
} else {
|
|
588
629
|
// No doc groups - auto-select first endpoint
|
|
589
|
-
setSelectedDocPage(null);
|
|
590
630
|
const firstEndpoint = collection ? getFirstEndpoint(collection) : null;
|
|
591
631
|
if (firstEndpoint) {
|
|
592
|
-
|
|
593
|
-
updateUrlHash(`endpoint/${firstEndpoint.id}`, tabId);
|
|
632
|
+
navigateToEndpoint(tabId, firstEndpoint.id);
|
|
594
633
|
} else {
|
|
595
|
-
|
|
596
|
-
updateUrlHash('', tabId);
|
|
634
|
+
navigateToTab(tabId);
|
|
597
635
|
}
|
|
598
636
|
}
|
|
599
637
|
} else {
|
|
600
638
|
// Switch to a doc group tab - find and select the first page in that tab
|
|
601
|
-
setSelectedRequest(null);
|
|
602
|
-
setSelectedDocSection(null);
|
|
603
639
|
// Find the first doc group for this tab and select its first page
|
|
604
640
|
if (collection?.docGroups) {
|
|
605
641
|
const tabDocGroup = collection.docGroups.find((g)=>isGroupForTab(g.id, tabId));
|
|
606
642
|
if (tabDocGroup && tabDocGroup.pages.length > 0) {
|
|
607
643
|
const firstPage = tabDocGroup.pages[0];
|
|
608
|
-
|
|
609
|
-
updateUrlHash(`page/${firstPage.slug}`, tabId);
|
|
644
|
+
navigateToPage(tabId, firstPage.slug);
|
|
610
645
|
switchToDocs();
|
|
611
646
|
} else {
|
|
612
|
-
|
|
613
|
-
updateUrlHash('', tabId);
|
|
647
|
+
navigateToTab(tabId);
|
|
614
648
|
}
|
|
615
649
|
}
|
|
616
650
|
}
|
|
617
651
|
}, [
|
|
618
652
|
collection,
|
|
619
|
-
updateUrlHash,
|
|
620
653
|
switchToDocs
|
|
621
654
|
]);
|
|
622
655
|
// Handler for agent navigation
|
|
@@ -624,19 +657,13 @@ function DocsContent() {
|
|
|
624
657
|
if (!collection) return;
|
|
625
658
|
const request = findRequestById(collection, endpointId);
|
|
626
659
|
if (request) {
|
|
627
|
-
//
|
|
628
|
-
|
|
629
|
-
// Then set the selected request
|
|
630
|
-
setSelectedRequest(request);
|
|
631
|
-
setSelectedDocSection(null);
|
|
632
|
-
setSelectedDocPage(null);
|
|
633
|
-
updateUrlHash(`endpoint/${endpointId}`, 'api-reference');
|
|
660
|
+
// Navigate to the endpoint in api-reference tab
|
|
661
|
+
navigateToEndpoint('api-reference', endpointId);
|
|
634
662
|
// Reset tab navigation so the new endpoint can determine its default tab
|
|
635
663
|
resetNavigation();
|
|
636
664
|
}
|
|
637
665
|
}, [
|
|
638
666
|
collection,
|
|
639
|
-
updateUrlHash,
|
|
640
667
|
resetNavigation
|
|
641
668
|
]);
|
|
642
669
|
// Handler for agent prefilling parameters
|
|
@@ -666,18 +693,7 @@ function DocsContent() {
|
|
|
666
693
|
const clearExplainContext = useCallback(()=>{
|
|
667
694
|
setExplainContext(null);
|
|
668
695
|
}, []);
|
|
669
|
-
//
|
|
670
|
-
useEffect(()=>{
|
|
671
|
-
if (!collection) return;
|
|
672
|
-
const handlePopState = ()=>{
|
|
673
|
-
navigateToHash(collection);
|
|
674
|
-
};
|
|
675
|
-
window.addEventListener('popstate', handlePopState);
|
|
676
|
-
return ()=>window.removeEventListener('popstate', handlePopState);
|
|
677
|
-
}, [
|
|
678
|
-
collection,
|
|
679
|
-
navigateToHash
|
|
680
|
-
]);
|
|
696
|
+
// Note: Browser back/forward is now handled by useRouteState hook automatically
|
|
681
697
|
// Dynamically set favicon from docs.json config
|
|
682
698
|
useEffect(()=>{
|
|
683
699
|
if (!collection?.docsFavicon) return;
|
|
@@ -733,184 +749,53 @@ function DocsContent() {
|
|
|
733
749
|
if (!selectedApiVersion && data?.selectedApiVersion) {
|
|
734
750
|
setSelectedApiVersion(data.selectedApiVersion);
|
|
735
751
|
}
|
|
736
|
-
// Only run initial navigation logic on first load
|
|
737
|
-
if (isInitialLoad) {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
752
|
+
// Only run initial navigation logic on first load
|
|
753
|
+
if (isInitialLoad && !hasInitialNavigated.current) {
|
|
754
|
+
hasInitialNavigated.current = true;
|
|
755
|
+
// Helper: Navigate to first content for a given tab
|
|
756
|
+
const navigateToFirstContent = (tabId)=>{
|
|
757
|
+
const tabConfig = data.navigationTabs?.find((t)=>t.id === tabId);
|
|
758
|
+
const isApiTab = tabConfig?.type === 'openapi' || tabConfig?.type === 'graphql' || tabId === 'api-reference';
|
|
759
|
+
// Try doc pages first, then endpoints
|
|
760
|
+
const firstPage = isApiTab ? getFirstDocPageForTab(data.docGroups, tabId) : data.docGroups?.find((g)=>isGroupForTab(g.id, tabId))?.pages[0]?.slug;
|
|
761
|
+
if (firstPage) {
|
|
762
|
+
navigateToPage(tabId, firstPage);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
// Fall back to first endpoint for API tabs
|
|
766
|
+
if (isApiTab) {
|
|
767
|
+
const firstEndpoint = getFirstEndpoint(data);
|
|
768
|
+
if (firstEndpoint) {
|
|
769
|
+
navigateToEndpoint(tabId, firstEndpoint.id);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
// No content found, just navigate to tab
|
|
774
|
+
navigateToTab(tabId);
|
|
775
|
+
};
|
|
776
|
+
// Get default tab (first tab in config)
|
|
777
|
+
let defaultTabId = 'api-reference';
|
|
778
|
+
if (data?.navigationTabs?.length > 0) {
|
|
741
779
|
const sortedTabs = [
|
|
742
780
|
...data.navigationTabs
|
|
743
781
|
].sort((a, b)=>a.order - b.order);
|
|
744
|
-
|
|
745
|
-
setActiveTab(initialTabId);
|
|
746
|
-
}
|
|
747
|
-
// Handle initial hash navigation after collection loads
|
|
748
|
-
if (data) {
|
|
749
|
-
// Use setTimeout to ensure state is set before navigation
|
|
750
|
-
setTimeout(()=>{
|
|
751
|
-
const hash = window.location.hash.slice(1);
|
|
752
|
-
if (!hash) {
|
|
753
|
-
// No hash - set URL to initial tab
|
|
754
|
-
setSelectedDocSection(null);
|
|
755
|
-
// Find the tab config to check its type
|
|
756
|
-
const tabConfig = data.navigationTabs?.find((t)=>t.id === initialTabId);
|
|
757
|
-
const isApiTab = tabConfig?.type === 'openapi' || tabConfig?.type === 'graphql' || initialTabId === 'api-reference';
|
|
758
|
-
if (isApiTab) {
|
|
759
|
-
// API Reference or GraphQL tab
|
|
760
|
-
setSelectedDocSection(null);
|
|
761
|
-
// Check if there are doc groups for this tab (MDX pages)
|
|
762
|
-
const hasGroups = hasDocGroupsForTab(data.docGroups, initialTabId);
|
|
763
|
-
if (hasGroups) {
|
|
764
|
-
// Has doc groups - select the first page from groups
|
|
765
|
-
const firstPage = getFirstDocPageForTab(data.docGroups, initialTabId);
|
|
766
|
-
if (firstPage) {
|
|
767
|
-
setSelectedDocPage(firstPage);
|
|
768
|
-
setSelectedRequest(null);
|
|
769
|
-
updateUrlHash(`page/${firstPage}`, initialTabId);
|
|
770
|
-
} else {
|
|
771
|
-
// No pages in groups - auto-select first endpoint
|
|
772
|
-
setSelectedDocPage(null);
|
|
773
|
-
const firstEndpoint = getFirstEndpoint(data);
|
|
774
|
-
if (firstEndpoint) {
|
|
775
|
-
setSelectedRequest(firstEndpoint);
|
|
776
|
-
updateUrlHash(`endpoint/${firstEndpoint.id}`, initialTabId);
|
|
777
|
-
} else {
|
|
778
|
-
setSelectedRequest(null);
|
|
779
|
-
updateUrlHash('', initialTabId);
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
} else {
|
|
783
|
-
// No doc groups - auto-select first endpoint
|
|
784
|
-
setSelectedDocPage(null);
|
|
785
|
-
const firstEndpoint = getFirstEndpoint(data);
|
|
786
|
-
if (firstEndpoint) {
|
|
787
|
-
setSelectedRequest(firstEndpoint);
|
|
788
|
-
updateUrlHash(`endpoint/${firstEndpoint.id}`, initialTabId);
|
|
789
|
-
} else {
|
|
790
|
-
setSelectedRequest(null);
|
|
791
|
-
updateUrlHash('', initialTabId);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
switchToDocs();
|
|
795
|
-
} else {
|
|
796
|
-
// Doc group tab - select first page
|
|
797
|
-
setSelectedRequest(null);
|
|
798
|
-
const tabDocGroup = data.docGroups?.find((g)=>isGroupForTab(g.id, initialTabId));
|
|
799
|
-
if (tabDocGroup && tabDocGroup.pages.length > 0) {
|
|
800
|
-
const firstPage = tabDocGroup.pages[0];
|
|
801
|
-
setSelectedDocPage(firstPage.slug);
|
|
802
|
-
updateUrlHash(`page/${firstPage.slug}`, initialTabId);
|
|
803
|
-
switchToDocs();
|
|
804
|
-
} else {
|
|
805
|
-
setSelectedDocPage(null);
|
|
806
|
-
updateUrlHash('', initialTabId);
|
|
807
|
-
switchToDocs();
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
} else if (hash === 'notes' || hash.startsWith('notes/')) {
|
|
811
|
-
// Notes mode - handled by ModeContext, just clear API selection
|
|
812
|
-
setSelectedRequest(null);
|
|
813
|
-
setSelectedDocSection(null);
|
|
814
|
-
} else {
|
|
815
|
-
// Parse the hash to get tab and content info
|
|
816
|
-
// Format: #tab or #tab/type/id (e.g., #api-reference/endpoint/123)
|
|
817
|
-
const parts = hash.split('/');
|
|
818
|
-
const hashTab = parts[0];
|
|
819
|
-
const hashType = parts[1];
|
|
820
|
-
const hashId = parts.slice(2).join('/');
|
|
821
|
-
// Set the tab from the hash
|
|
822
|
-
if (hashTab && data.navigationTabs?.some((t)=>t.id === hashTab)) {
|
|
823
|
-
setActiveTab(hashTab);
|
|
824
|
-
}
|
|
825
|
-
// Handle legacy format (endpoint/xxx without tab prefix)
|
|
826
|
-
const isLegacyFormat = hash.startsWith('endpoint/') || hash.startsWith('page/') || hash.startsWith('doc/');
|
|
827
|
-
const actualType = isLegacyFormat ? parts[0] : hashType;
|
|
828
|
-
const actualId = isLegacyFormat ? parts.slice(1).join('/') : hashId;
|
|
829
|
-
if (actualType === 'endpoint' && actualId) {
|
|
830
|
-
const request = findRequestById(data, actualId);
|
|
831
|
-
if (request) {
|
|
832
|
-
setSelectedRequest(request);
|
|
833
|
-
setSelectedDocSection(null);
|
|
834
|
-
setSelectedDocPage(null);
|
|
835
|
-
switchToDocs();
|
|
836
|
-
return;
|
|
837
|
-
} else {
|
|
838
|
-
setSelectedRequest(null);
|
|
839
|
-
setSelectedDocSection(null);
|
|
840
|
-
setSelectedDocPage(null);
|
|
841
|
-
switchToDocs();
|
|
842
|
-
}
|
|
843
|
-
} else if (actualType === 'page' && actualId) {
|
|
844
|
-
setSelectedDocPage(actualId);
|
|
845
|
-
setSelectedRequest(null);
|
|
846
|
-
setSelectedDocSection(null);
|
|
847
|
-
switchToDocs();
|
|
848
|
-
} else if (actualType === 'doc' && actualId) {
|
|
849
|
-
setSelectedDocSection(actualId);
|
|
850
|
-
setSelectedRequest(null);
|
|
851
|
-
setSelectedDocPage(null);
|
|
852
|
-
switchToDocs();
|
|
853
|
-
setTimeout(()=>{
|
|
854
|
-
const element = document.getElementById(actualId);
|
|
855
|
-
if (element) {
|
|
856
|
-
element.scrollIntoView({
|
|
857
|
-
behavior: 'smooth',
|
|
858
|
-
block: 'start'
|
|
859
|
-
});
|
|
860
|
-
}
|
|
861
|
-
}, 100);
|
|
862
|
-
} else if (hashTab && !hashType) {
|
|
863
|
-
// Just a tab, show its default content
|
|
864
|
-
setSelectedDocSection(null);
|
|
865
|
-
// Check if this is an API tab
|
|
866
|
-
const tabConfig = data.navigationTabs?.find((t)=>t.id === hashTab);
|
|
867
|
-
const isApiTab = tabConfig?.type === 'openapi' || tabConfig?.type === 'graphql' || hashTab === 'api-reference';
|
|
868
|
-
if (isApiTab) {
|
|
869
|
-
// Check if there are doc groups for this tab (MDX pages)
|
|
870
|
-
const hasGroups = hasDocGroupsForTab(data.docGroups, hashTab);
|
|
871
|
-
if (hasGroups) {
|
|
872
|
-
// Has doc groups - select the first page from groups
|
|
873
|
-
const firstPage = getFirstDocPageForTab(data.docGroups, hashTab);
|
|
874
|
-
if (firstPage) {
|
|
875
|
-
setSelectedDocPage(firstPage);
|
|
876
|
-
setSelectedRequest(null);
|
|
877
|
-
updateUrlHash(`page/${firstPage}`, hashTab);
|
|
878
|
-
} else {
|
|
879
|
-
// No pages in groups - auto-select first endpoint
|
|
880
|
-
setSelectedDocPage(null);
|
|
881
|
-
const firstEndpoint = getFirstEndpoint(data);
|
|
882
|
-
if (firstEndpoint) {
|
|
883
|
-
setSelectedRequest(firstEndpoint);
|
|
884
|
-
updateUrlHash(`endpoint/${firstEndpoint.id}`, hashTab);
|
|
885
|
-
} else {
|
|
886
|
-
setSelectedRequest(null);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
} else {
|
|
890
|
-
// No doc groups - auto-select first endpoint
|
|
891
|
-
setSelectedDocPage(null);
|
|
892
|
-
const firstEndpoint = getFirstEndpoint(data);
|
|
893
|
-
if (firstEndpoint) {
|
|
894
|
-
setSelectedRequest(firstEndpoint);
|
|
895
|
-
updateUrlHash(`endpoint/${firstEndpoint.id}`, hashTab);
|
|
896
|
-
} else {
|
|
897
|
-
setSelectedRequest(null);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
} else {
|
|
901
|
-
setSelectedDocPage(null);
|
|
902
|
-
setSelectedRequest(null);
|
|
903
|
-
}
|
|
904
|
-
switchToDocs();
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
}, 0);
|
|
782
|
+
defaultTabId = sortedTabs[0].id;
|
|
908
783
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
784
|
+
// Handle initial navigation after collection loads
|
|
785
|
+
setTimeout(()=>{
|
|
786
|
+
const hash = window.location.hash.slice(1);
|
|
787
|
+
const isTabOnly = hash && !hash.includes('/');
|
|
788
|
+
if (!hash) {
|
|
789
|
+
// No hash - navigate to first content of default tab
|
|
790
|
+
navigateToFirstContent(defaultTabId);
|
|
791
|
+
} else if (isTabOnly) {
|
|
792
|
+
// Just a tab name (e.g., #guides) - navigate to first content
|
|
793
|
+
navigateToFirstContent(hash);
|
|
794
|
+
}
|
|
795
|
+
// View mode (docs/playground/notes) is derived from URL by ModeProvider
|
|
796
|
+
}, 0);
|
|
913
797
|
}
|
|
798
|
+
// Version changes are handled by handleApiVersionChange callback
|
|
914
799
|
} catch (err) {
|
|
915
800
|
console.error('Error fetching collection:', err);
|
|
916
801
|
setError(err instanceof Error ? err.message : 'Failed to load API documentation');
|
|
@@ -922,7 +807,6 @@ function DocsContent() {
|
|
|
922
807
|
fetchCollection();
|
|
923
808
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
924
809
|
}, [
|
|
925
|
-
switchToDocs,
|
|
926
810
|
selectedApiVersion
|
|
927
811
|
]);
|
|
928
812
|
// Only show full-page loading on initial load
|
|
@@ -1165,10 +1049,29 @@ function ModeToggleTabs({ hasEndpoint }) {
|
|
|
1165
1049
|
function DocsWithMode({ collection, selectedRequest, selectedDocSection, selectedDocPage, activeTab, onTabChange, contentRef, showAuthModal, setShowAuthModal, handleSelectRequest, handleSelectDocumentation, handleSelectDocPage, handleDebugRequest, handleExplainRequest, handleAgentNavigate, handleAgentPrefill, debugContext, clearDebugContext, explainContext, clearExplainContext, navigateAndHighlight, selectedApiVersion, handleApiVersionChange, isVersionLoading, notFoundSlug }) {
|
|
1166
1050
|
const { mode, switchToDocs } = useModeContext();
|
|
1167
1051
|
const { setTheme } = useTheme();
|
|
1052
|
+
// Agent panel state - controls popup button and push animation
|
|
1053
|
+
const { isRightSidebarOpen, openRightSidebar } = useMobile();
|
|
1054
|
+
// Build endpoint index for suggestions (memoized)
|
|
1055
|
+
const endpointIndex = useMemo(()=>buildEndpointIndex(collection), [
|
|
1056
|
+
collection
|
|
1057
|
+
]);
|
|
1168
1058
|
// GraphQL state
|
|
1169
1059
|
const [graphqlOperations, setGraphqlOperations] = useState([]);
|
|
1170
1060
|
const [graphqlCollection, setGraphqlCollection] = useState(null);
|
|
1171
|
-
const [
|
|
1061
|
+
const [graphqlSchemaSDL, setGraphqlSchemaSDL] = useState(undefined);
|
|
1062
|
+
// Use route state for URL-based selection
|
|
1063
|
+
const routeState = useRouteState();
|
|
1064
|
+
// Derive selected GraphQL operation from URL
|
|
1065
|
+
const selectedGraphQLOperation = useMemo(()=>{
|
|
1066
|
+
if (routeState.contentType !== 'endpoint' || !routeState.contentId) {
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
return graphqlOperations.find((op)=>op.id === routeState.contentId) || null;
|
|
1070
|
+
}, [
|
|
1071
|
+
routeState.contentType,
|
|
1072
|
+
routeState.contentId,
|
|
1073
|
+
graphqlOperations
|
|
1074
|
+
]);
|
|
1172
1075
|
// Get API spec URL from environment or collection
|
|
1173
1076
|
const apiSpecUrl = process.env.NEXT_PUBLIC_OPENAPI_URL || collection.name || 'default';
|
|
1174
1077
|
// Check if there are endpoints
|
|
@@ -1193,7 +1096,7 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
|
|
|
1193
1096
|
}, []);
|
|
1194
1097
|
// Wrap agent navigate to switch to Docs mode and navigate to endpoint
|
|
1195
1098
|
const handleAgentNavigateWithModeSwitch = useCallback((endpointId)=>{
|
|
1196
|
-
//
|
|
1099
|
+
// handleAgentNavigate navigates via URL
|
|
1197
1100
|
handleAgentNavigate(endpointId);
|
|
1198
1101
|
}, [
|
|
1199
1102
|
handleAgentNavigate
|
|
@@ -1222,22 +1125,29 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
|
|
|
1222
1125
|
if (!showGraphQL || !schemaPath) {
|
|
1223
1126
|
setGraphqlOperations([]);
|
|
1224
1127
|
setGraphqlCollection(null);
|
|
1225
|
-
|
|
1128
|
+
setGraphqlSchemaSDL(undefined);
|
|
1226
1129
|
return;
|
|
1227
1130
|
}
|
|
1228
1131
|
// Load and parse GraphQL schema
|
|
1229
1132
|
const loadGraphQLSchema = async ()=>{
|
|
1230
1133
|
try {
|
|
1134
|
+
console.log('[DocsViewer] Loading GraphQL schema from:', schemaPath);
|
|
1231
1135
|
const response = await fetch(`/api/schema?path=${encodeURIComponent(schemaPath)}`);
|
|
1232
|
-
if (!response.ok)
|
|
1136
|
+
if (!response.ok) {
|
|
1137
|
+
console.error('[DocsViewer] Failed to fetch schema:', response.status);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1233
1140
|
const schemaContent = await response.text();
|
|
1141
|
+
console.log('[DocsViewer] Schema loaded, length:', schemaContent.length);
|
|
1142
|
+
setGraphqlSchemaSDL(schemaContent); // Store schema SDL for autocomplete
|
|
1234
1143
|
const operations = parseGraphQLSchema(schemaContent);
|
|
1144
|
+
console.log('[DocsViewer] Parsed operations:', operations.length);
|
|
1235
1145
|
setGraphqlOperations(operations);
|
|
1236
1146
|
// Convert to BrainfishCollection for sidebar
|
|
1237
1147
|
const gqlCollection = convertGraphQLToCollection(operations, schemaEndpoint || '');
|
|
1238
1148
|
setGraphqlCollection(gqlCollection);
|
|
1239
1149
|
} catch (err) {
|
|
1240
|
-
console.error('Failed to load GraphQL schema:', err);
|
|
1150
|
+
console.error('[DocsViewer] Failed to load GraphQL schema:', err);
|
|
1241
1151
|
}
|
|
1242
1152
|
};
|
|
1243
1153
|
loadGraphQLSchema();
|
|
@@ -1246,58 +1156,34 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
|
|
|
1246
1156
|
schemaPath,
|
|
1247
1157
|
schemaEndpoint
|
|
1248
1158
|
]);
|
|
1249
|
-
// Auto-
|
|
1159
|
+
// Auto-navigate to first GraphQL operation when none selected and no doc pages
|
|
1250
1160
|
useEffect(()=>{
|
|
1251
1161
|
if (!showGraphQL || graphqlOperations.length === 0) {
|
|
1252
1162
|
return;
|
|
1253
1163
|
}
|
|
1254
|
-
//
|
|
1255
|
-
|
|
1256
|
-
;
|
|
1257
|
-
if (hash) {
|
|
1258
|
-
const parts = hash.split('/');
|
|
1259
|
-
const hashType = parts[1];
|
|
1260
|
-
const hashId = parts.slice(2).join('/');
|
|
1261
|
-
if (hashType === 'endpoint' && hashId) {
|
|
1262
|
-
// Try to find and select the operation from the URL hash
|
|
1263
|
-
const operation = graphqlOperations.find((op)=>op.id === hashId);
|
|
1264
|
-
if (operation) {
|
|
1265
|
-
setSelectedGraphQLOperation(operation);
|
|
1266
|
-
return;
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
// If already have a selection, don't override it
|
|
1271
|
-
if (selectedGraphQLOperation) {
|
|
1164
|
+
// If there's already content selected (page or endpoint), don't auto-navigate
|
|
1165
|
+
if (routeState.contentType) {
|
|
1272
1166
|
return;
|
|
1273
1167
|
}
|
|
1274
|
-
// Check if there are doc groups for this tab
|
|
1168
|
+
// Check if there are doc groups for this tab
|
|
1275
1169
|
const hasGroups = hasDocGroupsForTab(collection?.docGroups, activeTab);
|
|
1276
1170
|
if (!hasGroups) {
|
|
1277
|
-
// No doc groups - auto-
|
|
1171
|
+
// No doc groups - auto-navigate to first GraphQL operation
|
|
1278
1172
|
const firstOperation = graphqlOperations[0];
|
|
1279
|
-
|
|
1280
|
-
window.history.pushState(null, '', `#${activeTab}/endpoint/${firstOperation.id}`);
|
|
1173
|
+
navigateToEndpoint(activeTab, firstOperation.id);
|
|
1281
1174
|
}
|
|
1282
1175
|
}, [
|
|
1283
1176
|
showGraphQL,
|
|
1284
1177
|
graphqlOperations,
|
|
1285
|
-
selectedGraphQLOperation,
|
|
1286
1178
|
activeTab,
|
|
1287
|
-
collection?.docGroups
|
|
1179
|
+
collection?.docGroups,
|
|
1180
|
+
routeState.contentType
|
|
1288
1181
|
]);
|
|
1289
|
-
// Handle GraphQL operation selection from sidebar
|
|
1182
|
+
// Handle GraphQL operation selection from sidebar - just navigates via URL
|
|
1290
1183
|
const handleSelectGraphQLOperation = useCallback((request)=>{
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
if (operation) {
|
|
1294
|
-
setSelectedGraphQLOperation(operation);
|
|
1295
|
-
// Update URL hash
|
|
1296
|
-
window.history.pushState(null, '', `#${activeTab}/endpoint/${request.id}`);
|
|
1297
|
-
switchToDocs();
|
|
1298
|
-
}
|
|
1184
|
+
navigateToEndpoint(activeTab, request.id);
|
|
1185
|
+
switchToDocs();
|
|
1299
1186
|
}, [
|
|
1300
|
-
graphqlOperations,
|
|
1301
1187
|
activeTab,
|
|
1302
1188
|
switchToDocs
|
|
1303
1189
|
]);
|
|
@@ -1381,7 +1267,8 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
|
|
|
1381
1267
|
docsNavbar: collection.docsNavbar
|
|
1382
1268
|
}),
|
|
1383
1269
|
/*#__PURE__*/ _jsxs("div", {
|
|
1384
|
-
className: "docs-layout flex flex-1 overflow-hidden relative z-0 min-h-0",
|
|
1270
|
+
className: cn("docs-layout flex flex-1 overflow-hidden relative z-0 min-h-0", "transition-[margin] duration-300 ease-in-out", // Push content left when agent panel is open (only on larger screens)
|
|
1271
|
+
isRightSidebarOpen && "lg:mr-96"),
|
|
1385
1272
|
children: [
|
|
1386
1273
|
!showChangelog && /*#__PURE__*/ _jsx(DocsSidebar, {
|
|
1387
1274
|
collection: {
|
|
@@ -1426,8 +1313,8 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
|
|
|
1426
1313
|
activeTab: activeTab,
|
|
1427
1314
|
children: /*#__PURE__*/ _jsx("div", {
|
|
1428
1315
|
ref: contentRef,
|
|
1429
|
-
className: cn("docs-content-area flex-1 bg-background min-w-0", showChangelog || showGraphQL && selectedGraphQLOperation ? "overflow-hidden flex flex-col" : "overflow-y-auto scroll-smooth"),
|
|
1430
|
-
children: showGraphQL && selectedGraphQLOperation ? /*#__PURE__*/ _jsx("div", {
|
|
1316
|
+
className: cn("docs-content-area flex-1 bg-background min-w-0", showChangelog || showGraphQL && selectedGraphQLOperation && !selectedDocPage ? "overflow-hidden flex flex-col" : "overflow-y-auto scroll-smooth"),
|
|
1317
|
+
children: showGraphQL && selectedGraphQLOperation && !selectedDocPage ? /*#__PURE__*/ _jsx("div", {
|
|
1431
1318
|
className: "flex-1 flex flex-col h-full",
|
|
1432
1319
|
children: /*#__PURE__*/ _jsx(GraphQLPlayground, {
|
|
1433
1320
|
endpoint: activeGraphQLSchemas[0]?.endpoint || '',
|
|
@@ -1436,7 +1323,8 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
|
|
|
1436
1323
|
selectedOperationId: selectedGraphQLOperation?.id,
|
|
1437
1324
|
hideExplorer: true,
|
|
1438
1325
|
headers: {},
|
|
1439
|
-
theme: "dark"
|
|
1326
|
+
theme: "dark",
|
|
1327
|
+
schemaSDL: graphqlSchemaSDL
|
|
1440
1328
|
})
|
|
1441
1329
|
}) : showChangelog ? /*#__PURE__*/ _jsx(ChangelogPage, {
|
|
1442
1330
|
releases: collection.changelogReleases || [],
|
|
@@ -1534,6 +1422,14 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
|
|
|
1534
1422
|
/*#__PURE__*/ _jsx(GlobalAuthModal, {
|
|
1535
1423
|
open: showAuthModal,
|
|
1536
1424
|
onClose: ()=>setShowAuthModal(false)
|
|
1425
|
+
}),
|
|
1426
|
+
!isRightSidebarOpen && /*#__PURE__*/ _jsx(AgentPopupButton, {
|
|
1427
|
+
onClick: openRightSidebar,
|
|
1428
|
+
currentEndpoint: selectedRequest ? {
|
|
1429
|
+
id: selectedRequest.id,
|
|
1430
|
+
name: selectedRequest.name
|
|
1431
|
+
} : null,
|
|
1432
|
+
endpointIndex: endpointIndex
|
|
1537
1433
|
})
|
|
1538
1434
|
]
|
|
1539
1435
|
});
|