@brainfish-ai/devdoc 0.1.48 → 0.1.49
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/deploy.js +16 -11
- package/package.json +1 -1
- package/renderer/app/[...slug]/client.js +17 -0
- package/renderer/app/[...slug]/page.js +125 -0
- package/renderer/app/api/assets/[...path]/route.js +23 -4
- package/renderer/app/api/chat/route.js +188 -25
- package/renderer/app/api/collections/route.js +95 -2
- package/renderer/app/api/deploy/route.js +4 -0
- package/renderer/app/api/suggestions/route.js +98 -10
- package/renderer/app/globals.css +33 -0
- package/renderer/app/layout.js +83 -8
- package/renderer/components/docs/mdx/cards.js +16 -45
- package/renderer/components/docs/mdx/file-tree.js +102 -0
- package/renderer/components/docs/mdx/index.js +7 -0
- package/renderer/components/docs-viewer/agent/agent-chat.js +75 -11
- package/renderer/components/docs-viewer/agent/messages/assistant-message.js +67 -3
- package/renderer/components/docs-viewer/agent/messages/tool-call-display.js +49 -4
- package/renderer/components/docs-viewer/content/content-router.js +1 -1
- package/renderer/components/docs-viewer/content/doc-page.js +36 -28
- package/renderer/components/docs-viewer/index.js +223 -58
- package/renderer/components/docs-viewer/playground/graphql-playground.js +131 -33
- package/renderer/components/docs-viewer/shared/method-badge.js +11 -2
- package/renderer/components/docs-viewer/sidebar/collection-tree.js +44 -6
- package/renderer/components/docs-viewer/sidebar/index.js +2 -1
- package/renderer/components/docs-viewer/sidebar/right-sidebar.js +3 -1
- package/renderer/components/docs-viewer/sidebar/sidebar-item.js +5 -7
- package/renderer/hooks/use-route-state.js +44 -56
- package/renderer/lib/api-docs/agent/indexer.js +73 -12
- package/renderer/lib/api-docs/agent/use-suggestions.js +26 -16
- package/renderer/lib/api-docs/code-editor/mode-context.js +16 -18
- package/renderer/lib/api-docs/parsers/openapi/transformer.js +8 -1
- package/renderer/lib/cache/purge.js +98 -0
- package/renderer/lib/docs-link-utils.js +146 -0
- package/renderer/lib/docs-navigation-context.js +3 -2
- package/renderer/lib/docs-navigation.js +50 -41
- package/renderer/lib/rate-limit.js +203 -0
|
@@ -44,7 +44,9 @@ export function ToolCallDisplay({ tool, onNavigate, onNavigateToDocPage, onOpenF
|
|
|
44
44
|
toolName === 'search_endpoints' && 'Searching...',
|
|
45
45
|
toolName === 'navigate_to_endpoint' && 'Navigating...',
|
|
46
46
|
toolName === 'prefill_parameters' && 'Setting params...',
|
|
47
|
+
toolName === 'prefill_graphql_variables' && 'Setting variables...',
|
|
47
48
|
toolName === 'get_endpoint_details' && 'Loading details...',
|
|
49
|
+
toolName === 'get_current_graphql_state' && 'Checking playground...',
|
|
48
50
|
toolName === 'check_request_validity' && 'Validating request...',
|
|
49
51
|
toolName === 'send_request' && /*#__PURE__*/ _jsxs("span", {
|
|
50
52
|
className: "flex items-center gap-1.5",
|
|
@@ -300,6 +302,48 @@ export function ToolCallDisplay({ tool, onNavigate, onNavigateToDocPage, onOpenF
|
|
|
300
302
|
]
|
|
301
303
|
});
|
|
302
304
|
}
|
|
305
|
+
// Prefill GraphQL variables
|
|
306
|
+
if (toolName === 'prefill_graphql_variables') {
|
|
307
|
+
const data = result || args;
|
|
308
|
+
const varCount = data.variables ? Object.keys(data.variables).length : 0;
|
|
309
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
310
|
+
className: "flex items-center gap-2 text-xs py-1.5 px-2.5 rounded-md bg-muted/50 border border-border/50",
|
|
311
|
+
children: [
|
|
312
|
+
/*#__PURE__*/ _jsx(CheckCircle, {
|
|
313
|
+
className: "size-3.5 text-green-600 dark:text-green-500",
|
|
314
|
+
weight: "fill"
|
|
315
|
+
}),
|
|
316
|
+
/*#__PURE__*/ _jsxs("span", {
|
|
317
|
+
className: "text-muted-foreground",
|
|
318
|
+
children: [
|
|
319
|
+
"Set ",
|
|
320
|
+
varCount,
|
|
321
|
+
" GraphQL variable",
|
|
322
|
+
varCount !== 1 ? 's' : ''
|
|
323
|
+
]
|
|
324
|
+
})
|
|
325
|
+
]
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
// Get current GraphQL state - show minimal indicator (the AI will describe the state)
|
|
329
|
+
if (toolName === 'get_current_graphql_state') {
|
|
330
|
+
const data = result;
|
|
331
|
+
if (!data?.success) return null // Don't show if failed
|
|
332
|
+
;
|
|
333
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
334
|
+
className: "flex items-center gap-2 text-xs py-1.5 px-2.5 rounded-md bg-muted/50 border border-border/50",
|
|
335
|
+
children: [
|
|
336
|
+
/*#__PURE__*/ _jsx(CheckCircle, {
|
|
337
|
+
className: "size-3.5 text-green-600 dark:text-green-500",
|
|
338
|
+
weight: "fill"
|
|
339
|
+
}),
|
|
340
|
+
/*#__PURE__*/ _jsx("span", {
|
|
341
|
+
className: "text-muted-foreground",
|
|
342
|
+
children: data?.hasResponse ? 'Read playground response' : 'Checked playground state'
|
|
343
|
+
})
|
|
344
|
+
]
|
|
345
|
+
});
|
|
346
|
+
}
|
|
303
347
|
// Search endpoints
|
|
304
348
|
if (toolName === 'search_endpoints') {
|
|
305
349
|
const results = result;
|
|
@@ -380,15 +424,16 @@ export function ToolCallDisplay({ tool, onNavigate, onNavigateToDocPage, onOpenF
|
|
|
380
424
|
// Get endpoint details
|
|
381
425
|
if (toolName === 'get_endpoint_details') {
|
|
382
426
|
const endpoint = result;
|
|
383
|
-
|
|
427
|
+
// Check for error or missing required fields
|
|
428
|
+
if (!endpoint || endpoint.error || !endpoint.name) return null;
|
|
384
429
|
return /*#__PURE__*/ _jsxs("div", {
|
|
385
430
|
className: "py-1.5 px-2.5 rounded-md bg-muted/50 border border-border/50",
|
|
386
431
|
children: [
|
|
387
432
|
/*#__PURE__*/ _jsxs("div", {
|
|
388
433
|
className: "flex items-center gap-2 text-xs",
|
|
389
434
|
children: [
|
|
390
|
-
/*#__PURE__*/ _jsx(MethodBadge, {
|
|
391
|
-
method: endpoint.method
|
|
435
|
+
endpoint.method && /*#__PURE__*/ _jsx(MethodBadge, {
|
|
436
|
+
method: endpoint.method,
|
|
392
437
|
size: "sm"
|
|
393
438
|
}),
|
|
394
439
|
/*#__PURE__*/ _jsx("span", {
|
|
@@ -397,7 +442,7 @@ export function ToolCallDisplay({ tool, onNavigate, onNavigateToDocPage, onOpenF
|
|
|
397
442
|
})
|
|
398
443
|
]
|
|
399
444
|
}),
|
|
400
|
-
/*#__PURE__*/ _jsx("code", {
|
|
445
|
+
endpoint.path && /*#__PURE__*/ _jsx("code", {
|
|
401
446
|
className: "text-[11px] text-muted-foreground block truncate mt-0.5",
|
|
402
447
|
children: endpoint.path
|
|
403
448
|
})
|
|
@@ -163,7 +163,7 @@ import { GraphQLPlayground } from '../playground/graphql-playground';
|
|
|
163
163
|
]
|
|
164
164
|
})
|
|
165
165
|
}) : /*#__PURE__*/ _jsx("div", {
|
|
166
|
-
className: "
|
|
166
|
+
className: "h-full min-h-[60vh] flex items-center justify-center bg-background",
|
|
167
167
|
children: /*#__PURE__*/ _jsxs("div", {
|
|
168
168
|
className: "text-center text-muted-foreground",
|
|
169
169
|
children: [
|
|
@@ -7,38 +7,41 @@ import { useDocsNavigation } from '@/lib/docs-navigation-context';
|
|
|
7
7
|
import { useCodeCopy } from '@/hooks/use-code-copy';
|
|
8
8
|
import { NotFoundPage } from './not-found-page';
|
|
9
9
|
import { MDXErrorBoundary } from './mdx-error-boundary';
|
|
10
|
+
import { getLinkAction, isExternalLink } from '@/lib/docs-link-utils';
|
|
10
11
|
// Custom Link component for MDX - uses docs navigation context
|
|
11
12
|
function MdxLink({ href, children, ...props }) {
|
|
12
13
|
const docsNav = useDocsNavigation();
|
|
13
14
|
const handleClick = useCallback((e)=>{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// Remove leading slash
|
|
24
|
-
if (slug.startsWith('/')) {
|
|
25
|
-
slug = slug.slice(1);
|
|
26
|
-
}
|
|
27
|
-
// Handle special paths
|
|
28
|
-
if (slug.startsWith('api-reference')) {
|
|
29
|
-
docsNav.switchToTab('api-reference');
|
|
15
|
+
if (!href) return;
|
|
16
|
+
const action = getLinkAction(href, {
|
|
17
|
+
activeTab: docsNav?.activeTab,
|
|
18
|
+
tabIds: docsNav?.tabIds
|
|
19
|
+
});
|
|
20
|
+
switch(action.type){
|
|
21
|
+
case 'external':
|
|
22
|
+
case 'anchor':
|
|
23
|
+
// Let default behavior handle these
|
|
30
24
|
return;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
case 'switchTab':
|
|
26
|
+
if (docsNav?.isApiDocsView) {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
docsNav.switchToTab(action.tabId);
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
case 'navigatePage':
|
|
32
|
+
if (docsNav?.isApiDocsView) {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
// Remove docs/ prefix if present
|
|
35
|
+
let slug = action.slug;
|
|
36
|
+
if (slug.startsWith('docs/')) {
|
|
37
|
+
slug = slug.slice(5);
|
|
38
|
+
}
|
|
39
|
+
docsNav.navigateToPage(slug);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
case 'navigate':
|
|
43
|
+
// Use default link behavior
|
|
34
44
|
return;
|
|
35
|
-
}
|
|
36
|
-
// Remove docs/ prefix if present
|
|
37
|
-
if (slug.startsWith('docs/')) {
|
|
38
|
-
slug = slug.slice(5);
|
|
39
|
-
}
|
|
40
|
-
// Navigate to the page
|
|
41
|
-
docsNav.navigateToPage(slug);
|
|
42
45
|
}
|
|
43
46
|
}, [
|
|
44
47
|
href,
|
|
@@ -48,7 +51,7 @@ function MdxLink({ href, children, ...props }) {
|
|
|
48
51
|
// Using visited: to override browser's purple visited link color
|
|
49
52
|
const linkClassName = "text-primary visited:text-primary font-medium underline decoration-primary/30 underline-offset-2 hover:decoration-primary transition-colors";
|
|
50
53
|
// External links
|
|
51
|
-
if (href &&
|
|
54
|
+
if (href && isExternalLink(href)) {
|
|
52
55
|
return /*#__PURE__*/ _jsx("a", {
|
|
53
56
|
href: href,
|
|
54
57
|
target: "_blank",
|
|
@@ -67,7 +70,8 @@ function MdxLink({ href, children, ...props }) {
|
|
|
67
70
|
});
|
|
68
71
|
}
|
|
69
72
|
// Import MDX components
|
|
70
|
-
import { Note, Warning, Info, Tip, Check, Error as ErrorCallout, Callout, Card, CardGroup, Accordion, AccordionGroup, Steps, Step, Tabs, Tab, CodeGroup, Frame, Columns, Snippet, Latex, ParamField, ResponseField, Expandable, Iframe, Video, Loom, Image, Screenshot, Logo, Icon, Highlight, Marker, Underline, Badge, Mermaid, PDF, Audio, Download, //
|
|
73
|
+
import { Note, Warning, Info, Tip, Check, Error as ErrorCallout, Callout, Card, CardGroup, Accordion, AccordionGroup, Steps, Step, Tabs, Tab, CodeGroup, Frame, Columns, Snippet, Latex, ParamField, ResponseField, Expandable, Iframe, Video, Loom, Image, Screenshot, Logo, Icon, Highlight, Marker, Underline, Badge, Mermaid, PDF, Audio, Download, // File Tree
|
|
74
|
+
FileTree, TreeFolder, TreeFile, // Landing Page Components
|
|
71
75
|
Hero, Pre, Tagline, Headline, Description, CommandBox, Section, Center, FeatureGrid, FeatureItem, ButtonLink, Spacer, Divider } from '../../docs/mdx/index';
|
|
72
76
|
// MDX components mapping
|
|
73
77
|
const mdxComponents = {
|
|
@@ -123,6 +127,10 @@ const mdxComponents = {
|
|
|
123
127
|
PDF,
|
|
124
128
|
Audio,
|
|
125
129
|
Download,
|
|
130
|
+
// File Tree
|
|
131
|
+
FileTree,
|
|
132
|
+
Folder: TreeFolder,
|
|
133
|
+
File: TreeFile,
|
|
126
134
|
// Landing Page
|
|
127
135
|
Hero,
|
|
128
136
|
Pre,
|
|
@@ -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 { buildSchema, isNonNullType, isListType } from 'graphql';
|
|
14
|
+
import { buildSchema, isNonNullType, isListType, isInputObjectType, isEnumType } 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';
|
|
@@ -31,7 +31,6 @@ import { useRouteState } from '@/hooks/use-route-state';
|
|
|
31
31
|
import { navigateToPage, navigateToEndpoint, navigateToSection, navigateToTab } from '@/lib/docs-navigation';
|
|
32
32
|
import { useMobile } from '@/lib/api-docs/mobile-context';
|
|
33
33
|
import { AgentPopupButton } from './agent/agent-popup-button';
|
|
34
|
-
import { buildEndpointIndex } from '@/lib/api-docs/agent/indexer';
|
|
35
34
|
// Helper to convert GraphQL type to string representation
|
|
36
35
|
function graphqlTypeToString(type) {
|
|
37
36
|
if (isNonNullType(type)) {
|
|
@@ -217,44 +216,70 @@ function generateExampleValue(type, schema, depth = 0) {
|
|
|
217
216
|
}
|
|
218
217
|
// Check if it's an enum type
|
|
219
218
|
const schemaType = schema.getType(typeName);
|
|
220
|
-
if (schemaType &&
|
|
221
|
-
const
|
|
222
|
-
const values = enumType.getValues();
|
|
219
|
+
if (schemaType && isEnumType(schemaType)) {
|
|
220
|
+
const values = schemaType.getValues();
|
|
223
221
|
if (values.length > 0) {
|
|
224
222
|
return values[0].name // Return first enum value
|
|
225
223
|
;
|
|
226
224
|
}
|
|
227
225
|
}
|
|
228
|
-
// Check if it's an input object type
|
|
229
|
-
if (schemaType &&
|
|
230
|
-
const
|
|
231
|
-
const fields = inputType.getFields();
|
|
226
|
+
// Check if it's an input object type (for filter inputs, etc.)
|
|
227
|
+
if (schemaType && isInputObjectType(schemaType)) {
|
|
228
|
+
const fields = schemaType.getFields();
|
|
232
229
|
const result = {};
|
|
233
|
-
//
|
|
230
|
+
// First, include all required fields
|
|
234
231
|
for (const [fieldName, field] of Object.entries(fields)){
|
|
235
232
|
if (isNonNullType(field.type)) {
|
|
236
|
-
// Use default value if available, otherwise generate
|
|
237
233
|
if (field.defaultValue !== undefined) {
|
|
238
234
|
result[fieldName] = field.defaultValue;
|
|
239
235
|
} else {
|
|
240
|
-
|
|
236
|
+
const value = generateExampleValue(field.type, schema, depth + 1);
|
|
237
|
+
if (value !== null) {
|
|
238
|
+
result[fieldName] = value;
|
|
239
|
+
}
|
|
241
240
|
}
|
|
242
241
|
}
|
|
243
242
|
}
|
|
244
|
-
// If no required fields, include
|
|
243
|
+
// If no required fields, include a useful optional field
|
|
244
|
+
// Prioritize common filter/query fields
|
|
245
245
|
if (Object.keys(result).length === 0) {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
246
|
+
const fieldEntries = Object.entries(fields);
|
|
247
|
+
// Priority order for filter fields
|
|
248
|
+
const priorityFields = [
|
|
249
|
+
'eq',
|
|
250
|
+
'equals',
|
|
251
|
+
'id',
|
|
252
|
+
'code',
|
|
253
|
+
'name',
|
|
254
|
+
'in',
|
|
255
|
+
'contains'
|
|
256
|
+
];
|
|
257
|
+
let selectedField;
|
|
258
|
+
// Find a priority field first
|
|
259
|
+
for (const pf of priorityFields){
|
|
260
|
+
const found = fieldEntries.find(([name])=>name.toLowerCase() === pf);
|
|
261
|
+
if (found) {
|
|
262
|
+
selectedField = found;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Fall back to first field
|
|
267
|
+
if (!selectedField && fieldEntries.length > 0) {
|
|
268
|
+
selectedField = fieldEntries[0];
|
|
269
|
+
}
|
|
270
|
+
if (selectedField) {
|
|
271
|
+
const [fname, fdef] = selectedField;
|
|
250
272
|
if (fdef.defaultValue !== undefined) {
|
|
251
273
|
result[fname] = fdef.defaultValue;
|
|
252
274
|
} else {
|
|
253
|
-
|
|
275
|
+
const value = generateExampleValue(fdef.type, schema, depth + 1);
|
|
276
|
+
if (value !== null) {
|
|
277
|
+
result[fname] = value;
|
|
278
|
+
}
|
|
254
279
|
}
|
|
255
280
|
}
|
|
256
281
|
}
|
|
257
|
-
return result;
|
|
282
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
258
283
|
}
|
|
259
284
|
// Unknown type - return null
|
|
260
285
|
return null;
|
|
@@ -657,14 +682,27 @@ function DocsContent() {
|
|
|
657
682
|
if (!collection) return;
|
|
658
683
|
const request = findRequestById(collection, endpointId);
|
|
659
684
|
if (request) {
|
|
660
|
-
//
|
|
661
|
-
|
|
685
|
+
// Find the correct tab for the endpoint
|
|
686
|
+
// If current tab is an OpenAPI tab, use it; otherwise find the first OpenAPI tab
|
|
687
|
+
const currentTabConfig = collection.navigationTabs?.find((t)=>t.id === activeTab);
|
|
688
|
+
const isCurrentTabOpenApi = currentTabConfig?.type === 'openapi' || currentTabConfig?.type === 'graphql';
|
|
689
|
+
let targetTab = activeTab;
|
|
690
|
+
if (!isCurrentTabOpenApi) {
|
|
691
|
+
// Find the first OpenAPI tab
|
|
692
|
+
const firstOpenApiTab = collection.navigationTabs?.find((t)=>t.type === 'openapi' || t.type === 'graphql');
|
|
693
|
+
if (firstOpenApiTab) {
|
|
694
|
+
targetTab = firstOpenApiTab.id;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
// Navigate to the endpoint in the correct tab
|
|
698
|
+
navigateToEndpoint(targetTab, endpointId);
|
|
662
699
|
// Reset tab navigation so the new endpoint can determine its default tab
|
|
663
700
|
resetNavigation();
|
|
664
701
|
}
|
|
665
702
|
}, [
|
|
666
703
|
collection,
|
|
667
|
-
resetNavigation
|
|
704
|
+
resetNavigation,
|
|
705
|
+
activeTab
|
|
668
706
|
]);
|
|
669
707
|
// Handler for agent prefilling parameters
|
|
670
708
|
const handleAgentPrefill = useCallback((data)=>{
|
|
@@ -790,15 +828,18 @@ function DocsContent() {
|
|
|
790
828
|
}
|
|
791
829
|
// Handle initial navigation after collection loads
|
|
792
830
|
setTimeout(()=>{
|
|
793
|
-
const
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
831
|
+
const pathname = window.location.pathname;
|
|
832
|
+
const pathParts = pathname.split('/').filter(Boolean);
|
|
833
|
+
const isTabOnly = pathParts.length === 1 // Just /tab-name
|
|
834
|
+
;
|
|
835
|
+
if (pathParts.length === 0) {
|
|
836
|
+
// No path - navigate to first content of default tab
|
|
797
837
|
navigateToFirstContent(defaultTabId);
|
|
798
838
|
} else if (isTabOnly) {
|
|
799
|
-
// Just a tab name (e.g.,
|
|
800
|
-
navigateToFirstContent(
|
|
839
|
+
// Just a tab name (e.g., /facebook-pages-api) - navigate to first content
|
|
840
|
+
navigateToFirstContent(pathParts[0]);
|
|
801
841
|
}
|
|
842
|
+
// If path has content (e.g., /tab/page/slug), no auto-navigation needed
|
|
802
843
|
// View mode (docs/playground/notes) is derived from URL by ModeProvider
|
|
803
844
|
}, 0);
|
|
804
845
|
}
|
|
@@ -1069,14 +1110,71 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
|
|
|
1069
1110
|
const { setTheme } = useTheme();
|
|
1070
1111
|
// Agent panel state - controls popup button and push animation
|
|
1071
1112
|
const { isRightSidebarOpen, openRightSidebar } = useMobile();
|
|
1072
|
-
//
|
|
1073
|
-
|
|
1074
|
-
|
|
1113
|
+
// Memoize currentEndpoint for AgentPopupButton to prevent unnecessary re-renders
|
|
1114
|
+
// Using primitive deps intentionally to avoid re-creating object on every render
|
|
1115
|
+
const currentEndpointForPopup = useMemo(()=>selectedRequest ? {
|
|
1116
|
+
id: selectedRequest.id,
|
|
1117
|
+
name: selectedRequest.name
|
|
1118
|
+
} : null, // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1119
|
+
[
|
|
1120
|
+
selectedRequest?.id,
|
|
1121
|
+
selectedRequest?.name
|
|
1075
1122
|
]);
|
|
1076
1123
|
// GraphQL state
|
|
1077
1124
|
const [graphqlOperations, setGraphqlOperations] = useState([]);
|
|
1078
1125
|
const [graphqlCollection, setGraphqlCollection] = useState(null);
|
|
1079
1126
|
const [graphqlSchemaSDL, setGraphqlSchemaSDL] = useState(undefined);
|
|
1127
|
+
// External GraphQL variables (set by AI agent prefill tool)
|
|
1128
|
+
const [externalGraphQLVariables, setExternalGraphQLVariables] = useState(undefined);
|
|
1129
|
+
// Current GraphQL playground state (for AI context)
|
|
1130
|
+
const [graphqlPlaygroundState, setGraphqlPlaygroundState] = useState(null);
|
|
1131
|
+
// Handler for AI agent prefilling GraphQL variables
|
|
1132
|
+
const handlePrefillGraphQLVariables = useCallback((variables)=>{
|
|
1133
|
+
console.log('[Agent] GraphQL variables prefill requested:', variables);
|
|
1134
|
+
setExternalGraphQLVariables(variables);
|
|
1135
|
+
}, []);
|
|
1136
|
+
// Handler for GraphQL playground state changes (for AI context)
|
|
1137
|
+
const handleGraphQLStateChange = useCallback((state)=>{
|
|
1138
|
+
setGraphqlPlaygroundState(state);
|
|
1139
|
+
}, []);
|
|
1140
|
+
// Handler for GraphQL debug requests - convert to DebugContext format
|
|
1141
|
+
const handleGraphQLDebugRequest = useCallback((context)=>{
|
|
1142
|
+
const hasErrors = context.response?.errors && context.response.errors.length > 0;
|
|
1143
|
+
const debugCtx = {
|
|
1144
|
+
status: hasErrors ? 400 : 200,
|
|
1145
|
+
statusText: hasErrors ? 'GraphQL Error' : 'OK',
|
|
1146
|
+
responseBody: context.response ? JSON.stringify(context.response, null, 2) : '',
|
|
1147
|
+
errorMessage: context.error || context.response?.errors?.map((e)=>e.message).join(', '),
|
|
1148
|
+
endpointName: context.operationName || 'GraphQL Query',
|
|
1149
|
+
endpointMethod: 'QUERY',
|
|
1150
|
+
endpointPath: 'graphql',
|
|
1151
|
+
requestBody: JSON.stringify({
|
|
1152
|
+
query: context.query,
|
|
1153
|
+
variables: context.variables
|
|
1154
|
+
}, null, 2)
|
|
1155
|
+
};
|
|
1156
|
+
handleDebugRequest(debugCtx);
|
|
1157
|
+
}, [
|
|
1158
|
+
handleDebugRequest
|
|
1159
|
+
]);
|
|
1160
|
+
// Handler for GraphQL explain requests - convert to DebugContext format
|
|
1161
|
+
const handleGraphQLExplainRequest = useCallback((context)=>{
|
|
1162
|
+
const explainCtx = {
|
|
1163
|
+
status: 200,
|
|
1164
|
+
statusText: 'OK',
|
|
1165
|
+
responseBody: context.response ? JSON.stringify(context.response, null, 2) : '',
|
|
1166
|
+
endpointName: context.operationName || 'GraphQL Query',
|
|
1167
|
+
endpointMethod: 'QUERY',
|
|
1168
|
+
endpointPath: 'graphql',
|
|
1169
|
+
requestBody: JSON.stringify({
|
|
1170
|
+
query: context.query,
|
|
1171
|
+
variables: context.variables
|
|
1172
|
+
}, null, 2)
|
|
1173
|
+
};
|
|
1174
|
+
handleExplainRequest(explainCtx);
|
|
1175
|
+
}, [
|
|
1176
|
+
handleExplainRequest
|
|
1177
|
+
]);
|
|
1080
1178
|
// Use route state for URL-based selection
|
|
1081
1179
|
const routeState = useRouteState();
|
|
1082
1180
|
// Derive selected GraphQL operation from URL
|
|
@@ -1112,13 +1210,6 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
|
|
|
1112
1210
|
window.location.hash = hashPath;
|
|
1113
1211
|
}
|
|
1114
1212
|
}, []);
|
|
1115
|
-
// Wrap agent navigate to switch to Docs mode and navigate to endpoint
|
|
1116
|
-
const handleAgentNavigateWithModeSwitch = useCallback((endpointId)=>{
|
|
1117
|
-
// handleAgentNavigate navigates via URL
|
|
1118
|
-
handleAgentNavigate(endpointId);
|
|
1119
|
-
}, [
|
|
1120
|
-
handleAgentNavigate
|
|
1121
|
-
]);
|
|
1122
1213
|
// Get the current content title for header
|
|
1123
1214
|
// Find the active tab's config
|
|
1124
1215
|
const activeTabConfig = collection.navigationTabs?.find((t)=>t.id === activeTab);
|
|
@@ -1135,12 +1226,21 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
|
|
|
1135
1226
|
// Show GraphQL playground when graphql tab is active
|
|
1136
1227
|
const showGraphQL = activeTabType === 'graphql';
|
|
1137
1228
|
const activeGraphQLSchemas = activeTabConfig?.graphqlSchemas || [];
|
|
1138
|
-
// Get the first schema
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
|
|
1229
|
+
// Get the first GraphQL schema from any tab (for playground/sidebar)
|
|
1230
|
+
const firstGraphQLSchema = useMemo(()=>{
|
|
1231
|
+
for (const tab of collection.navigationTabs || []){
|
|
1232
|
+
if (tab.type === 'graphql' && tab.graphqlSchemas?.[0]) {
|
|
1233
|
+
return tab.graphqlSchemas[0];
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return null;
|
|
1237
|
+
}, [
|
|
1238
|
+
collection.navigationTabs
|
|
1239
|
+
]);
|
|
1240
|
+
// Always load GraphQL schema when available (for AI agent search/navigate)
|
|
1241
|
+
// This ensures GraphQL endpoints are available regardless of active tab
|
|
1142
1242
|
useEffect(()=>{
|
|
1143
|
-
if (!
|
|
1243
|
+
if (!firstGraphQLSchema?.schema) {
|
|
1144
1244
|
setGraphqlOperations([]);
|
|
1145
1245
|
setGraphqlCollection(null);
|
|
1146
1246
|
setGraphqlSchemaSDL(undefined);
|
|
@@ -1149,30 +1249,89 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
|
|
|
1149
1249
|
// Load and parse GraphQL schema
|
|
1150
1250
|
const loadGraphQLSchema = async ()=>{
|
|
1151
1251
|
try {
|
|
1152
|
-
|
|
1153
|
-
const response = await fetch(`/api/schema?path=${encodeURIComponent(schemaPath)}`);
|
|
1252
|
+
const response = await fetch(`/api/schema?path=${encodeURIComponent(firstGraphQLSchema.schema)}`);
|
|
1154
1253
|
if (!response.ok) {
|
|
1155
|
-
console.error('[DocsViewer] Failed to fetch schema:', response.status);
|
|
1156
1254
|
return;
|
|
1157
1255
|
}
|
|
1158
1256
|
const schemaContent = await response.text();
|
|
1159
|
-
console.log('[DocsViewer] Schema loaded, length:', schemaContent.length);
|
|
1160
1257
|
setGraphqlSchemaSDL(schemaContent); // Store schema SDL for autocomplete
|
|
1161
1258
|
const operations = parseGraphQLSchema(schemaContent);
|
|
1162
|
-
console.log('[DocsViewer] Parsed operations:', operations.length);
|
|
1163
1259
|
setGraphqlOperations(operations);
|
|
1164
1260
|
// Convert to BrainfishCollection for sidebar
|
|
1165
|
-
const gqlCollection = convertGraphQLToCollection(operations,
|
|
1261
|
+
const gqlCollection = convertGraphQLToCollection(operations, firstGraphQLSchema.endpoint || '');
|
|
1166
1262
|
setGraphqlCollection(gqlCollection);
|
|
1167
1263
|
} catch (err) {
|
|
1168
1264
|
console.error('[DocsViewer] Failed to load GraphQL schema:', err);
|
|
1169
1265
|
}
|
|
1170
1266
|
};
|
|
1171
1267
|
loadGraphQLSchema();
|
|
1268
|
+
}, [
|
|
1269
|
+
firstGraphQLSchema?.schema,
|
|
1270
|
+
firstGraphQLSchema?.endpoint
|
|
1271
|
+
]);
|
|
1272
|
+
// Active collection - GraphQL collection when GraphQL tab is active, otherwise REST collection
|
|
1273
|
+
// This is used by sidebar, agent chat, and other components that need the current API context
|
|
1274
|
+
const activeCollection = useMemo(()=>{
|
|
1275
|
+
return showGraphQL && graphqlCollection ? graphqlCollection : collection;
|
|
1172
1276
|
}, [
|
|
1173
1277
|
showGraphQL,
|
|
1174
|
-
|
|
1175
|
-
|
|
1278
|
+
graphqlCollection,
|
|
1279
|
+
collection
|
|
1280
|
+
]);
|
|
1281
|
+
// Active request - the currently selected endpoint (REST or GraphQL)
|
|
1282
|
+
const activeRequest = useMemo(()=>{
|
|
1283
|
+
if (showGraphQL && selectedGraphQLOperation && graphqlCollection) {
|
|
1284
|
+
return findRequestById(graphqlCollection, selectedGraphQLOperation.id);
|
|
1285
|
+
}
|
|
1286
|
+
return selectedRequest;
|
|
1287
|
+
}, [
|
|
1288
|
+
showGraphQL,
|
|
1289
|
+
selectedGraphQLOperation,
|
|
1290
|
+
graphqlCollection,
|
|
1291
|
+
selectedRequest
|
|
1292
|
+
]);
|
|
1293
|
+
// Use endpoint index from Collections API (already includes REST + GraphQL)
|
|
1294
|
+
// This is pre-built server-side for better performance and consistency
|
|
1295
|
+
const endpointIndex = collection.endpointIndex || [];
|
|
1296
|
+
// Wrap agent navigate to switch to Docs mode and navigate to endpoint
|
|
1297
|
+
// Uses activeCollection to support both REST and GraphQL endpoints
|
|
1298
|
+
const handleAgentNavigateWithModeSwitch = useCallback((endpointId)=>{
|
|
1299
|
+
// First check if the endpoint exists in the active collection (current tab)
|
|
1300
|
+
const requestInActive = findRequestById(activeCollection, endpointId);
|
|
1301
|
+
if (requestInActive) {
|
|
1302
|
+
navigateToEndpoint(activeTab, endpointId);
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
// Check if it's in the GraphQL collection (might be on different tab)
|
|
1306
|
+
if (graphqlCollection) {
|
|
1307
|
+
const requestInGraphQL = findRequestById(graphqlCollection, endpointId);
|
|
1308
|
+
if (requestInGraphQL) {
|
|
1309
|
+
// Find the GraphQL tab and navigate there
|
|
1310
|
+
const graphqlTab = collection.navigationTabs?.find((t)=>t.type === 'graphql');
|
|
1311
|
+
if (graphqlTab) {
|
|
1312
|
+
navigateToEndpoint(graphqlTab.id, endpointId);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
// Check if it's in the REST collection (might be on different tab)
|
|
1318
|
+
const requestInREST = findRequestById(collection, endpointId);
|
|
1319
|
+
if (requestInREST) {
|
|
1320
|
+
// Find the first OpenAPI tab and navigate there
|
|
1321
|
+
const openApiTab = collection.navigationTabs?.find((t)=>t.type === 'openapi');
|
|
1322
|
+
if (openApiTab) {
|
|
1323
|
+
navigateToEndpoint(openApiTab.id, endpointId);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
// Fall back to parent's navigate
|
|
1328
|
+
handleAgentNavigate(endpointId);
|
|
1329
|
+
}, [
|
|
1330
|
+
activeCollection,
|
|
1331
|
+
activeTab,
|
|
1332
|
+
graphqlCollection,
|
|
1333
|
+
collection,
|
|
1334
|
+
handleAgentNavigate
|
|
1176
1335
|
]);
|
|
1177
1336
|
// Auto-navigate to first GraphQL operation when none selected and no doc pages
|
|
1178
1337
|
useEffect(()=>{
|
|
@@ -1329,16 +1488,21 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
|
|
|
1329
1488
|
onNavigateToPage: handleSelectDocPage,
|
|
1330
1489
|
onSwitchTab: onTabChange,
|
|
1331
1490
|
activeTab: activeTab,
|
|
1491
|
+
tabIds: collection.navigationTabs?.map((t)=>t.id),
|
|
1332
1492
|
children: /*#__PURE__*/ _jsx("div", {
|
|
1333
1493
|
ref: contentRef,
|
|
1334
1494
|
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"),
|
|
1335
1495
|
children: showGraphQL && selectedGraphQLOperation && !selectedDocPage ? /*#__PURE__*/ _jsx("div", {
|
|
1336
1496
|
className: "flex-1 flex flex-col h-full",
|
|
1337
1497
|
children: /*#__PURE__*/ _jsx(GraphQLPlayground, {
|
|
1338
|
-
endpoint: activeGraphQLSchemas[0]?.endpoint || '',
|
|
1498
|
+
endpoint: activeGraphQLSchemas[0]?.endpoint || firstGraphQLSchema?.endpoint || '',
|
|
1339
1499
|
defaultQuery: selectedGraphQLOperation?.query,
|
|
1340
1500
|
operations: graphqlOperations,
|
|
1341
1501
|
selectedOperationId: selectedGraphQLOperation?.id,
|
|
1502
|
+
externalVariables: externalGraphQLVariables,
|
|
1503
|
+
onStateChange: handleGraphQLStateChange,
|
|
1504
|
+
onDebugRequest: handleGraphQLDebugRequest,
|
|
1505
|
+
onExplainRequest: handleGraphQLExplainRequest,
|
|
1342
1506
|
hideExplorer: true,
|
|
1343
1507
|
headers: {},
|
|
1344
1508
|
theme: "dark",
|
|
@@ -1402,11 +1566,13 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
|
|
|
1402
1566
|
]
|
|
1403
1567
|
}),
|
|
1404
1568
|
/*#__PURE__*/ _jsx(RightSidebar, {
|
|
1405
|
-
request:
|
|
1569
|
+
request: activeRequest,
|
|
1406
1570
|
collection: collection,
|
|
1407
1571
|
apiSummary: collection.apiSummary,
|
|
1408
1572
|
onNavigateToEndpoint: handleAgentNavigateWithModeSwitch,
|
|
1409
1573
|
onPrefillParameters: handleAgentPrefill,
|
|
1574
|
+
onPrefillGraphQLVariables: handlePrefillGraphQLVariables,
|
|
1575
|
+
graphqlPlaygroundState: graphqlPlaygroundState,
|
|
1410
1576
|
debugContext: debugContext,
|
|
1411
1577
|
onClearDebugContext: clearDebugContext,
|
|
1412
1578
|
explainContext: explainContext,
|
|
@@ -1444,17 +1610,16 @@ function DocsWithMode({ collection, selectedRequest, selectedDocSection, selecte
|
|
|
1444
1610
|
!isRightSidebarOpen && /*#__PURE__*/ _jsx(AgentPopupButton, {
|
|
1445
1611
|
onClick: ()=>openRightSidebar(),
|
|
1446
1612
|
onQuestionClick: (prompt)=>openRightSidebar(prompt),
|
|
1447
|
-
currentEndpoint:
|
|
1448
|
-
id: selectedRequest.id,
|
|
1449
|
-
name: selectedRequest.name
|
|
1450
|
-
} : null,
|
|
1613
|
+
currentEndpoint: currentEndpointForPopup,
|
|
1451
1614
|
endpointIndex: endpointIndex
|
|
1452
1615
|
})
|
|
1453
1616
|
]
|
|
1454
1617
|
});
|
|
1455
1618
|
}
|
|
1456
1619
|
// Wrapper component with providers
|
|
1457
|
-
export function Docs() {
|
|
1620
|
+
export function Docs({ initialPath } = {}) {
|
|
1621
|
+
// initialPath is passed for SSR but useRouteState reads from usePathname() automatically
|
|
1622
|
+
void initialPath; // Acknowledge prop (used for SSR metadata matching)
|
|
1458
1623
|
return /*#__PURE__*/ _jsx(AuthProvider, {
|
|
1459
1624
|
children: /*#__PURE__*/ _jsx(ModeProvider, {
|
|
1460
1625
|
children: /*#__PURE__*/ _jsx(PlaygroundProvider, {
|