@brainfish-ai/devdoc 0.1.21
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/LICENSE +33 -0
- package/README.md +415 -0
- package/bin/devdoc.js +13 -0
- package/dist/cli/commands/build.d.ts +5 -0
- package/dist/cli/commands/build.js +87 -0
- package/dist/cli/commands/check.d.ts +1 -0
- package/dist/cli/commands/check.js +143 -0
- package/dist/cli/commands/create.d.ts +24 -0
- package/dist/cli/commands/create.js +387 -0
- package/dist/cli/commands/deploy.d.ts +9 -0
- package/dist/cli/commands/deploy.js +433 -0
- package/dist/cli/commands/dev.d.ts +6 -0
- package/dist/cli/commands/dev.js +139 -0
- package/dist/cli/commands/init.d.ts +11 -0
- package/dist/cli/commands/init.js +238 -0
- package/dist/cli/commands/keys.d.ts +12 -0
- package/dist/cli/commands/keys.js +165 -0
- package/dist/cli/commands/start.d.ts +5 -0
- package/dist/cli/commands/start.js +56 -0
- package/dist/cli/commands/upload.d.ts +13 -0
- package/dist/cli/commands/upload.js +238 -0
- package/dist/cli/commands/whoami.d.ts +8 -0
- package/dist/cli/commands/whoami.js +91 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +106 -0
- package/dist/config/index.d.ts +80 -0
- package/dist/config/index.js +133 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +13 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +61 -0
- package/dist/utils/paths.d.ts +16 -0
- package/dist/utils/paths.js +50 -0
- package/package.json +51 -0
- package/renderer/app/api/assets/[...path]/route.ts +123 -0
- package/renderer/app/api/assets/route.ts +124 -0
- package/renderer/app/api/assets/upload/route.ts +177 -0
- package/renderer/app/api/auth-schemes/route.ts +77 -0
- package/renderer/app/api/chat/route.ts +858 -0
- package/renderer/app/api/codegen/route.ts +72 -0
- package/renderer/app/api/collections/route.ts +1016 -0
- package/renderer/app/api/debug/route.ts +53 -0
- package/renderer/app/api/deploy/route.ts +234 -0
- package/renderer/app/api/device/route.ts +42 -0
- package/renderer/app/api/docs/route.ts +187 -0
- package/renderer/app/api/keys/regenerate/route.ts +80 -0
- package/renderer/app/api/openapi-spec/route.ts +151 -0
- package/renderer/app/api/projects/[slug]/route.ts +153 -0
- package/renderer/app/api/projects/[slug]/stats/route.ts +96 -0
- package/renderer/app/api/projects/register/route.ts +152 -0
- package/renderer/app/api/proxy/route.ts +149 -0
- package/renderer/app/api/proxy-stream/route.ts +168 -0
- package/renderer/app/api/redirects/route.ts +47 -0
- package/renderer/app/api/schema/route.ts +65 -0
- package/renderer/app/api/subdomains/check/route.ts +172 -0
- package/renderer/app/api/suggestions/route.ts +144 -0
- package/renderer/app/favicon.ico +0 -0
- package/renderer/app/globals.css +1103 -0
- package/renderer/app/layout.tsx +47 -0
- package/renderer/app/llms-full.txt/route.ts +346 -0
- package/renderer/app/llms.txt/route.ts +279 -0
- package/renderer/app/page.tsx +14 -0
- package/renderer/app/robots.txt/route.ts +84 -0
- package/renderer/app/sitemap.xml/route.ts +199 -0
- package/renderer/components/docs/index.ts +12 -0
- package/renderer/components/docs/mdx/accordion.tsx +169 -0
- package/renderer/components/docs/mdx/badge.tsx +132 -0
- package/renderer/components/docs/mdx/callouts.tsx +154 -0
- package/renderer/components/docs/mdx/cards.tsx +213 -0
- package/renderer/components/docs/mdx/changelog.tsx +120 -0
- package/renderer/components/docs/mdx/code-block.tsx +186 -0
- package/renderer/components/docs/mdx/code-group.tsx +421 -0
- package/renderer/components/docs/mdx/file-embeds.tsx +105 -0
- package/renderer/components/docs/mdx/frame.tsx +112 -0
- package/renderer/components/docs/mdx/highlight.tsx +151 -0
- package/renderer/components/docs/mdx/iframe.tsx +134 -0
- package/renderer/components/docs/mdx/image.tsx +235 -0
- package/renderer/components/docs/mdx/index.ts +204 -0
- package/renderer/components/docs/mdx/mermaid.tsx +240 -0
- package/renderer/components/docs/mdx/param-field.tsx +200 -0
- package/renderer/components/docs/mdx/steps.tsx +113 -0
- package/renderer/components/docs/mdx/tabs.tsx +86 -0
- package/renderer/components/docs/mdx-renderer.tsx +100 -0
- package/renderer/components/docs/navigation/breadcrumbs.tsx +76 -0
- package/renderer/components/docs/navigation/index.ts +8 -0
- package/renderer/components/docs/navigation/page-nav.tsx +64 -0
- package/renderer/components/docs/navigation/sidebar.tsx +515 -0
- package/renderer/components/docs/navigation/toc.tsx +113 -0
- package/renderer/components/docs/notice.tsx +105 -0
- package/renderer/components/docs-header.tsx +274 -0
- package/renderer/components/docs-viewer/agent/agent-chat.tsx +2076 -0
- package/renderer/components/docs-viewer/agent/cards/debug-context-card.tsx +90 -0
- package/renderer/components/docs-viewer/agent/cards/endpoint-context-card.tsx +49 -0
- package/renderer/components/docs-viewer/agent/cards/index.tsx +50 -0
- package/renderer/components/docs-viewer/agent/cards/response-options-card.tsx +212 -0
- package/renderer/components/docs-viewer/agent/cards/types.ts +84 -0
- package/renderer/components/docs-viewer/agent/chat-message.tsx +17 -0
- package/renderer/components/docs-viewer/agent/index.tsx +6 -0
- package/renderer/components/docs-viewer/agent/messages/assistant-message.tsx +119 -0
- package/renderer/components/docs-viewer/agent/messages/chat-message.tsx +46 -0
- package/renderer/components/docs-viewer/agent/messages/index.ts +17 -0
- package/renderer/components/docs-viewer/agent/messages/tool-call-display.tsx +721 -0
- package/renderer/components/docs-viewer/agent/messages/types.ts +61 -0
- package/renderer/components/docs-viewer/agent/messages/typing-indicator.tsx +24 -0
- package/renderer/components/docs-viewer/agent/messages/user-message.tsx +51 -0
- package/renderer/components/docs-viewer/code-editor/index.tsx +2 -0
- package/renderer/components/docs-viewer/code-editor/notes-mode.tsx +1283 -0
- package/renderer/components/docs-viewer/content/changelog-page.tsx +331 -0
- package/renderer/components/docs-viewer/content/doc-page.tsx +285 -0
- package/renderer/components/docs-viewer/content/documentation-viewer.tsx +17 -0
- package/renderer/components/docs-viewer/content/index.tsx +29 -0
- package/renderer/components/docs-viewer/content/introduction.tsx +21 -0
- package/renderer/components/docs-viewer/content/request-details.tsx +330 -0
- package/renderer/components/docs-viewer/content/sections/auth.tsx +69 -0
- package/renderer/components/docs-viewer/content/sections/body.tsx +66 -0
- package/renderer/components/docs-viewer/content/sections/headers.tsx +43 -0
- package/renderer/components/docs-viewer/content/sections/overview.tsx +40 -0
- package/renderer/components/docs-viewer/content/sections/parameters.tsx +43 -0
- package/renderer/components/docs-viewer/content/sections/responses.tsx +87 -0
- package/renderer/components/docs-viewer/global-auth-modal.tsx +352 -0
- package/renderer/components/docs-viewer/index.tsx +1466 -0
- package/renderer/components/docs-viewer/playground/auth-editor.tsx +280 -0
- package/renderer/components/docs-viewer/playground/body-editor.tsx +221 -0
- package/renderer/components/docs-viewer/playground/code-editor.tsx +224 -0
- package/renderer/components/docs-viewer/playground/code-snippet.tsx +387 -0
- package/renderer/components/docs-viewer/playground/graphql-playground.tsx +745 -0
- package/renderer/components/docs-viewer/playground/index.tsx +671 -0
- package/renderer/components/docs-viewer/playground/key-value-editor.tsx +261 -0
- package/renderer/components/docs-viewer/playground/method-selector.tsx +60 -0
- package/renderer/components/docs-viewer/playground/request-builder.tsx +179 -0
- package/renderer/components/docs-viewer/playground/request-tabs.tsx +237 -0
- package/renderer/components/docs-viewer/playground/response-cards/idle-card.tsx +21 -0
- package/renderer/components/docs-viewer/playground/response-cards/index.tsx +93 -0
- package/renderer/components/docs-viewer/playground/response-cards/loading-card.tsx +16 -0
- package/renderer/components/docs-viewer/playground/response-cards/network-error-card.tsx +23 -0
- package/renderer/components/docs-viewer/playground/response-cards/response-body-card.tsx +268 -0
- package/renderer/components/docs-viewer/playground/response-cards/types.ts +82 -0
- package/renderer/components/docs-viewer/playground/response-viewer.tsx +43 -0
- package/renderer/components/docs-viewer/search/index.ts +2 -0
- package/renderer/components/docs-viewer/search/search-dialog.tsx +331 -0
- package/renderer/components/docs-viewer/search/use-search.ts +117 -0
- package/renderer/components/docs-viewer/shared/markdown-renderer.tsx +431 -0
- package/renderer/components/docs-viewer/shared/method-badge.tsx +41 -0
- package/renderer/components/docs-viewer/shared/schema-viewer.tsx +349 -0
- package/renderer/components/docs-viewer/sidebar/collection-tree.tsx +239 -0
- package/renderer/components/docs-viewer/sidebar/endpoint-options.tsx +316 -0
- package/renderer/components/docs-viewer/sidebar/index.tsx +343 -0
- package/renderer/components/docs-viewer/sidebar/right-sidebar.tsx +202 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-group.tsx +118 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-item.tsx +226 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-section.tsx +52 -0
- package/renderer/components/theme-provider.tsx +11 -0
- package/renderer/components/theme-toggle.tsx +76 -0
- package/renderer/components/ui/badge.tsx +46 -0
- package/renderer/components/ui/button.tsx +59 -0
- package/renderer/components/ui/dialog.tsx +118 -0
- package/renderer/components/ui/dropdown-menu.tsx +257 -0
- package/renderer/components/ui/input.tsx +21 -0
- package/renderer/components/ui/label.tsx +24 -0
- package/renderer/components/ui/navigation-menu.tsx +168 -0
- package/renderer/components/ui/select.tsx +190 -0
- package/renderer/components/ui/spinner.tsx +114 -0
- package/renderer/components/ui/tabs.tsx +66 -0
- package/renderer/components/ui/tooltip.tsx +61 -0
- package/renderer/hooks/use-code-copy.ts +88 -0
- package/renderer/hooks/use-openapi-title.ts +44 -0
- package/renderer/lib/api-docs/agent/index.ts +6 -0
- package/renderer/lib/api-docs/agent/indexer.ts +323 -0
- package/renderer/lib/api-docs/agent/spec-summary.ts +335 -0
- package/renderer/lib/api-docs/agent/types.ts +116 -0
- package/renderer/lib/api-docs/auth/auth-context.tsx +225 -0
- package/renderer/lib/api-docs/auth/auth-storage.ts +87 -0
- package/renderer/lib/api-docs/auth/crypto.ts +89 -0
- package/renderer/lib/api-docs/auth/index.ts +4 -0
- package/renderer/lib/api-docs/code-editor/db.ts +164 -0
- package/renderer/lib/api-docs/code-editor/hooks.ts +266 -0
- package/renderer/lib/api-docs/code-editor/index.ts +6 -0
- package/renderer/lib/api-docs/code-editor/mode-context.tsx +207 -0
- package/renderer/lib/api-docs/code-editor/types.ts +105 -0
- package/renderer/lib/api-docs/codegen/definitions.ts +297 -0
- package/renderer/lib/api-docs/codegen/har.ts +251 -0
- package/renderer/lib/api-docs/codegen/index.ts +159 -0
- package/renderer/lib/api-docs/factories.ts +151 -0
- package/renderer/lib/api-docs/index.ts +17 -0
- package/renderer/lib/api-docs/mobile-context.tsx +112 -0
- package/renderer/lib/api-docs/navigation-context.tsx +88 -0
- package/renderer/lib/api-docs/parsers/graphql/README.md +129 -0
- package/renderer/lib/api-docs/parsers/graphql/index.ts +91 -0
- package/renderer/lib/api-docs/parsers/graphql/parser.ts +491 -0
- package/renderer/lib/api-docs/parsers/graphql/transformer.ts +246 -0
- package/renderer/lib/api-docs/parsers/graphql/types.ts +283 -0
- package/renderer/lib/api-docs/parsers/openapi/README.md +32 -0
- package/renderer/lib/api-docs/parsers/openapi/dereferencer.ts +60 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/auth.ts +574 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/body.ts +403 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/index.ts +232 -0
- package/renderer/lib/api-docs/parsers/openapi/index.ts +171 -0
- package/renderer/lib/api-docs/parsers/openapi/transformer.ts +277 -0
- package/renderer/lib/api-docs/parsers/openapi/validator.ts +31 -0
- package/renderer/lib/api-docs/playground/context.tsx +107 -0
- package/renderer/lib/api-docs/playground/navigation-context.tsx +124 -0
- package/renderer/lib/api-docs/playground/request-builder.ts +223 -0
- package/renderer/lib/api-docs/playground/request-runner.ts +282 -0
- package/renderer/lib/api-docs/playground/types.ts +35 -0
- package/renderer/lib/api-docs/types.ts +269 -0
- package/renderer/lib/api-docs/utils.ts +311 -0
- package/renderer/lib/cache.ts +193 -0
- package/renderer/lib/docs/config/index.ts +29 -0
- package/renderer/lib/docs/config/loader.ts +142 -0
- package/renderer/lib/docs/config/schema.ts +298 -0
- package/renderer/lib/docs/index.ts +12 -0
- package/renderer/lib/docs/mdx/compiler.ts +176 -0
- package/renderer/lib/docs/mdx/frontmatter.ts +80 -0
- package/renderer/lib/docs/mdx/index.ts +26 -0
- package/renderer/lib/docs/navigation/generator.ts +348 -0
- package/renderer/lib/docs/navigation/index.ts +12 -0
- package/renderer/lib/docs/navigation/types.ts +123 -0
- package/renderer/lib/docs-navigation-context.tsx +80 -0
- package/renderer/lib/multi-tenant/context.ts +105 -0
- package/renderer/lib/storage/blob.ts +845 -0
- package/renderer/lib/utils.ts +6 -0
- package/renderer/next.config.ts +76 -0
- package/renderer/package.json +66 -0
- package/renderer/postcss.config.mjs +5 -0
- package/renderer/public/assets/images/screenshot.png +0 -0
- package/renderer/public/assets/logo/dark.svg +9 -0
- package/renderer/public/assets/logo/light.svg +9 -0
- package/renderer/public/assets/logo.svg +9 -0
- package/renderer/public/file.svg +1 -0
- package/renderer/public/globe.svg +1 -0
- package/renderer/public/icon.png +0 -0
- package/renderer/public/logo.svg +9 -0
- package/renderer/public/window.svg +1 -0
- package/renderer/tsconfig.json +28 -0
- package/templates/basic/README.md +139 -0
- package/templates/basic/assets/favicon.svg +4 -0
- package/templates/basic/assets/logo.svg +9 -0
- package/templates/basic/docs.json +47 -0
- package/templates/basic/guides/configuration.mdx +149 -0
- package/templates/basic/guides/overview.mdx +96 -0
- package/templates/basic/index.mdx +39 -0
- package/templates/basic/package.json +14 -0
- package/templates/basic/quickstart.mdx +92 -0
- package/templates/basic/vercel.json +6 -0
- package/templates/graphql/README.md +139 -0
- package/templates/graphql/api-reference/schema.graphql +305 -0
- package/templates/graphql/assets/favicon.svg +4 -0
- package/templates/graphql/assets/logo.svg +9 -0
- package/templates/graphql/docs.json +54 -0
- package/templates/graphql/guides/configuration.mdx +149 -0
- package/templates/graphql/guides/overview.mdx +96 -0
- package/templates/graphql/index.mdx +39 -0
- package/templates/graphql/package.json +14 -0
- package/templates/graphql/quickstart.mdx +92 -0
- package/templates/graphql/vercel.json +6 -0
- package/templates/openapi/README.md +139 -0
- package/templates/openapi/api-reference/openapi.json +419 -0
- package/templates/openapi/assets/favicon.svg +4 -0
- package/templates/openapi/assets/logo.svg +9 -0
- package/templates/openapi/docs.json +61 -0
- package/templates/openapi/guides/configuration.mdx +149 -0
- package/templates/openapi/guides/overview.mdx +96 -0
- package/templates/openapi/index.mdx +39 -0
- package/templates/openapi/package.json +14 -0
- package/templates/openapi/quickstart.mdx +92 -0
- package/templates/openapi/vercel.json +6 -0
|
@@ -0,0 +1,1466 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback, useRef } from 'react'
|
|
4
|
+
import { Spinner } from '@phosphor-icons/react'
|
|
5
|
+
import type { BrainfishCollection, BrainfishRESTRequest, BrainfishDocGroup } from '@/lib/api-docs/types'
|
|
6
|
+
import { DocsSidebar } from './sidebar'
|
|
7
|
+
import { ApiPlayground } from './playground'
|
|
8
|
+
import type { DebugContext } from './playground/response-viewer'
|
|
9
|
+
import { RightSidebar } from './sidebar/right-sidebar'
|
|
10
|
+
import { Introduction } from './content/introduction'
|
|
11
|
+
import { RequestDetails } from './content/request-details'
|
|
12
|
+
import { DocPage } from './content/doc-page'
|
|
13
|
+
import { ChangelogPage } from './content/changelog-page'
|
|
14
|
+
import { GraphQLPlayground, type GraphQLOperationItem } from './playground/graphql-playground'
|
|
15
|
+
import { makeBrainfishCollection } from '@/lib/api-docs/factories'
|
|
16
|
+
import { parse, Kind, type DocumentNode, type FieldDefinitionNode, type InputValueDefinitionNode, type TypeNode } from 'graphql'
|
|
17
|
+
import { GlobalAuthModal } from './global-auth-modal'
|
|
18
|
+
import { AuthProvider } from '@/lib/api-docs/auth'
|
|
19
|
+
import { PlaygroundProvider, usePlaygroundPrefill } from '@/lib/api-docs/playground/context'
|
|
20
|
+
import { PlaygroundNavigationProvider, usePlaygroundNavigation, type PlaygroundTab, type HighlightField } from '@/lib/api-docs/playground/navigation-context'
|
|
21
|
+
import { NavigationProvider } from '@/lib/api-docs/navigation-context'
|
|
22
|
+
import type { PrefillData } from '@/lib/api-docs/agent/types'
|
|
23
|
+
import { ModeProvider, useModeContext } from '@/lib/api-docs/code-editor'
|
|
24
|
+
import { NotesMode } from './code-editor'
|
|
25
|
+
import { DocsHeader } from '@/components/docs-header'
|
|
26
|
+
import { Notice } from '../docs/notice'
|
|
27
|
+
import { DocsNavigationProvider } from '@/lib/docs-navigation-context'
|
|
28
|
+
import { Button } from '@/components/ui/button'
|
|
29
|
+
import { Code, TestTube, Book } from '@phosphor-icons/react'
|
|
30
|
+
import { cn } from '@/lib/utils'
|
|
31
|
+
import { SearchDialog, useSearch } from './search'
|
|
32
|
+
import { useTheme } from 'next-themes'
|
|
33
|
+
|
|
34
|
+
// Helper to convert GraphQL TypeNode to string
|
|
35
|
+
function typeNodeToString(typeNode: TypeNode): string {
|
|
36
|
+
switch (typeNode.kind) {
|
|
37
|
+
case Kind.NAMED_TYPE:
|
|
38
|
+
return typeNode.name.value
|
|
39
|
+
case Kind.NON_NULL_TYPE:
|
|
40
|
+
return `${typeNodeToString(typeNode.type)}!`
|
|
41
|
+
case Kind.LIST_TYPE:
|
|
42
|
+
return `[${typeNodeToString(typeNode.type)}]`
|
|
43
|
+
default:
|
|
44
|
+
return 'Unknown'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Parse GraphQL schema using the graphql library
|
|
49
|
+
function parseGraphQLSchema(schemaSDL: string): GraphQLOperationItem[] {
|
|
50
|
+
const operations: GraphQLOperationItem[] = []
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const ast: DocumentNode = parse(schemaSDL)
|
|
54
|
+
|
|
55
|
+
// Find Query, Mutation, Subscription type definitions
|
|
56
|
+
for (const def of ast.definitions) {
|
|
57
|
+
if (def.kind === Kind.OBJECT_TYPE_DEFINITION) {
|
|
58
|
+
const typeName = def.name.value
|
|
59
|
+
|
|
60
|
+
// Only process root operation types
|
|
61
|
+
if (!['Query', 'Mutation', 'Subscription'].includes(typeName)) continue
|
|
62
|
+
|
|
63
|
+
const operationType = typeName.toLowerCase() as 'query' | 'mutation' | 'subscription'
|
|
64
|
+
|
|
65
|
+
// Process each field
|
|
66
|
+
for (const field of def.fields || []) {
|
|
67
|
+
const name = field.name.value
|
|
68
|
+
|
|
69
|
+
// Skip internal fields
|
|
70
|
+
if (name.startsWith('_')) continue
|
|
71
|
+
|
|
72
|
+
// Get description
|
|
73
|
+
const description = field.description?.value || null
|
|
74
|
+
|
|
75
|
+
// Get return type
|
|
76
|
+
const returnType = typeNodeToString(field.type)
|
|
77
|
+
|
|
78
|
+
// Build args string for query generation
|
|
79
|
+
const args = (field.arguments || [])
|
|
80
|
+
.map((arg: InputValueDefinitionNode) => `${arg.name.value}: ${typeNodeToString(arg.type)}`)
|
|
81
|
+
.join(', ')
|
|
82
|
+
|
|
83
|
+
const query = generateGraphQLQuery(operationType, name, field.arguments || [], returnType)
|
|
84
|
+
const exampleVariables = generateGraphQLVariables(field.arguments || [])
|
|
85
|
+
|
|
86
|
+
operations.push({
|
|
87
|
+
id: `${operationType}-${name}`,
|
|
88
|
+
name,
|
|
89
|
+
description,
|
|
90
|
+
operationType,
|
|
91
|
+
query,
|
|
92
|
+
exampleVariables,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error('[GraphQL Parser] Failed to parse schema:', err)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return operations
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Generate example GraphQL query from AST
|
|
106
|
+
function generateGraphQLQuery(
|
|
107
|
+
operationType: string,
|
|
108
|
+
name: string,
|
|
109
|
+
args: readonly InputValueDefinitionNode[],
|
|
110
|
+
returnType: string
|
|
111
|
+
): string {
|
|
112
|
+
let query = `${operationType} ${name.charAt(0).toUpperCase() + name.slice(1)}`
|
|
113
|
+
|
|
114
|
+
if (args.length > 0) {
|
|
115
|
+
const varDefs = args.map(arg => `$${arg.name.value}: ${typeNodeToString(arg.type)}`).join(', ')
|
|
116
|
+
query += `(${varDefs})`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
query += ` {\n ${name}`
|
|
120
|
+
|
|
121
|
+
if (args.length > 0) {
|
|
122
|
+
const argPairs = args.map(arg => `${arg.name.value}: $${arg.name.value}`).join(', ')
|
|
123
|
+
query += `(${argPairs})`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const baseType = returnType.replace(/[\[\]!]/g, '').trim()
|
|
127
|
+
if (['String', 'Int', 'Float', 'Boolean', 'ID'].includes(baseType)) {
|
|
128
|
+
query += '\n}'
|
|
129
|
+
} else {
|
|
130
|
+
query += ` {\n id\n __typename\n }\n}`
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return query
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Generate example variables from AST args
|
|
137
|
+
function generateGraphQLVariables(args: readonly InputValueDefinitionNode[]): Record<string, unknown> {
|
|
138
|
+
const variables: Record<string, unknown> = {}
|
|
139
|
+
|
|
140
|
+
for (const arg of args) {
|
|
141
|
+
const name = arg.name.value
|
|
142
|
+
const type = typeNodeToString(arg.type).replace(/[\[\]!]/g, '').trim()
|
|
143
|
+
|
|
144
|
+
switch (type) {
|
|
145
|
+
case 'String': variables[name] = 'example'; break
|
|
146
|
+
case 'Int': variables[name] = 1; break
|
|
147
|
+
case 'Float': variables[name] = 1.0; break
|
|
148
|
+
case 'Boolean': variables[name] = true; break
|
|
149
|
+
case 'ID': variables[name] = '1'; break
|
|
150
|
+
default: variables[name] = {}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return variables
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Convert GraphQL operations to BrainfishCollection for sidebar
|
|
158
|
+
function convertGraphQLToCollection(
|
|
159
|
+
operations: GraphQLOperationItem[],
|
|
160
|
+
endpoint: string
|
|
161
|
+
): BrainfishCollection {
|
|
162
|
+
const queries = operations.filter(op => op.operationType === 'query')
|
|
163
|
+
const mutations = operations.filter(op => op.operationType === 'mutation')
|
|
164
|
+
const subscriptions = operations.filter(op => op.operationType === 'subscription')
|
|
165
|
+
|
|
166
|
+
const toRequest = (op: GraphQLOperationItem): BrainfishRESTRequest => ({
|
|
167
|
+
id: op.id,
|
|
168
|
+
name: op.name,
|
|
169
|
+
description: op.description || '',
|
|
170
|
+
method: 'POST',
|
|
171
|
+
endpoint,
|
|
172
|
+
params: [],
|
|
173
|
+
headers: [],
|
|
174
|
+
auth: { authType: 'none', authActive: false },
|
|
175
|
+
body: { contentType: 'application/json', body: JSON.stringify({ query: op.query, variables: op.exampleVariables }, null, 2) },
|
|
176
|
+
requestVariables: [],
|
|
177
|
+
responses: {},
|
|
178
|
+
tags: [op.operationType],
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const folders: BrainfishCollection[] = []
|
|
182
|
+
|
|
183
|
+
if (queries.length > 0) {
|
|
184
|
+
folders.push(makeBrainfishCollection({
|
|
185
|
+
name: 'Queries',
|
|
186
|
+
description: 'GraphQL Query operations',
|
|
187
|
+
requests: queries.map(toRequest),
|
|
188
|
+
folders: [],
|
|
189
|
+
variables: [],
|
|
190
|
+
auth: { authType: 'none', authActive: false },
|
|
191
|
+
headers: [],
|
|
192
|
+
}))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (mutations.length > 0) {
|
|
196
|
+
folders.push(makeBrainfishCollection({
|
|
197
|
+
name: 'Mutations',
|
|
198
|
+
description: 'GraphQL Mutation operations',
|
|
199
|
+
requests: mutations.map(toRequest),
|
|
200
|
+
folders: [],
|
|
201
|
+
variables: [],
|
|
202
|
+
auth: { authType: 'none', authActive: false },
|
|
203
|
+
headers: [],
|
|
204
|
+
}))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (subscriptions.length > 0) {
|
|
208
|
+
folders.push(makeBrainfishCollection({
|
|
209
|
+
name: 'Subscriptions',
|
|
210
|
+
description: 'GraphQL Subscription operations',
|
|
211
|
+
requests: subscriptions.map(toRequest),
|
|
212
|
+
folders: [],
|
|
213
|
+
variables: [],
|
|
214
|
+
auth: { authType: 'none', authActive: false },
|
|
215
|
+
headers: [],
|
|
216
|
+
}))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return makeBrainfishCollection({
|
|
220
|
+
name: 'GraphQL API',
|
|
221
|
+
description: 'GraphQL operations',
|
|
222
|
+
folders,
|
|
223
|
+
requests: [],
|
|
224
|
+
variables: [],
|
|
225
|
+
auth: { authType: 'none', authActive: false },
|
|
226
|
+
headers: [],
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Helper to find request by ID in collection
|
|
231
|
+
function findRequestById(collection: BrainfishCollection, id: string): BrainfishRESTRequest | null {
|
|
232
|
+
// Check direct requests
|
|
233
|
+
const found = collection.requests.find(r => r.id === id)
|
|
234
|
+
if (found) return found
|
|
235
|
+
|
|
236
|
+
// Check folders recursively
|
|
237
|
+
for (const folder of collection.folders) {
|
|
238
|
+
const request = findRequestById(folder, id)
|
|
239
|
+
if (request) return request
|
|
240
|
+
}
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// API version
|
|
245
|
+
interface ApiVersion {
|
|
246
|
+
version: string
|
|
247
|
+
spec: string
|
|
248
|
+
default: boolean
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// GraphQL schema info for graphql tabs
|
|
252
|
+
interface GraphQLSchemaInfo {
|
|
253
|
+
name: string
|
|
254
|
+
schema: string
|
|
255
|
+
endpoint: string
|
|
256
|
+
default: boolean
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Navigation tab from config
|
|
260
|
+
interface NavigationTab {
|
|
261
|
+
id: string
|
|
262
|
+
tab: string // Tab name (used for both navigation and page header)
|
|
263
|
+
type: 'docs' | 'openapi' | 'changelog' | 'graphql'
|
|
264
|
+
path?: string
|
|
265
|
+
order: number
|
|
266
|
+
versions?: ApiVersion[] // Available API versions (for openapi type)
|
|
267
|
+
graphqlSchemas?: GraphQLSchemaInfo[] // GraphQL schemas (for graphql type)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
interface ChangelogRelease {
|
|
271
|
+
version: string
|
|
272
|
+
date: string
|
|
273
|
+
title: string
|
|
274
|
+
slug: string
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Extended collection type with apiSummary from server
|
|
278
|
+
interface DocsLogo {
|
|
279
|
+
url?: string
|
|
280
|
+
alt?: string
|
|
281
|
+
width?: number
|
|
282
|
+
height?: number
|
|
283
|
+
light?: string
|
|
284
|
+
dark?: string
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
interface DocsHeaderConfig {
|
|
288
|
+
showSearch?: boolean
|
|
289
|
+
showThemeToggle?: boolean
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
interface DocsNavbarLink {
|
|
293
|
+
label: string
|
|
294
|
+
href: string
|
|
295
|
+
external?: boolean
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
interface DocsNavbarPrimary {
|
|
299
|
+
label: string
|
|
300
|
+
href: string
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
interface DocsNavbar {
|
|
304
|
+
links?: DocsNavbarLink[]
|
|
305
|
+
primary?: DocsNavbarPrimary
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
interface DocsColors {
|
|
309
|
+
primary?: string
|
|
310
|
+
primaryLight?: string
|
|
311
|
+
primaryDark?: string
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
interface NoticeConfig {
|
|
315
|
+
content: string
|
|
316
|
+
dismissible?: boolean
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
interface CollectionWithSummary extends BrainfishCollection {
|
|
320
|
+
apiSummary?: string | null
|
|
321
|
+
specVersion?: string
|
|
322
|
+
docsName?: string | null
|
|
323
|
+
docsFavicon?: string | null
|
|
324
|
+
docsLogo?: DocsLogo | null
|
|
325
|
+
docsHeader?: DocsHeaderConfig | null
|
|
326
|
+
docsNavbar?: DocsNavbar | null
|
|
327
|
+
docsColors?: DocsColors | null
|
|
328
|
+
defaultTheme?: 'light' | 'dark' | 'system' | null
|
|
329
|
+
customCss?: string | null
|
|
330
|
+
navigationTabs?: NavigationTab[]
|
|
331
|
+
changelogReleases?: ChangelogRelease[]
|
|
332
|
+
apiVersions?: ApiVersion[]
|
|
333
|
+
selectedApiVersion?: string
|
|
334
|
+
notice?: NoticeConfig | null
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function DocsContent() {
|
|
338
|
+
const [collection, setCollection] = useState<CollectionWithSummary | null>(null)
|
|
339
|
+
const [selectedRequest, setSelectedRequest] = useState<BrainfishRESTRequest | null>(null)
|
|
340
|
+
const [selectedDocSection, setSelectedDocSection] = useState<string | null>(null)
|
|
341
|
+
const [selectedDocPage, setSelectedDocPage] = useState<string | null>(null)
|
|
342
|
+
const [activeTab, setActiveTab] = useState<string>('api-reference')
|
|
343
|
+
const [selectedApiVersion, setSelectedApiVersion] = useState<string | null>(null)
|
|
344
|
+
const [loading, setLoading] = useState(true)
|
|
345
|
+
const [isVersionLoading, setIsVersionLoading] = useState(false) // For version switch only
|
|
346
|
+
const [error, setError] = useState<string | null>(null)
|
|
347
|
+
const [showAuthModal, setShowAuthModal] = useState(false)
|
|
348
|
+
|
|
349
|
+
// Prefill context for agent
|
|
350
|
+
const { setPrefill } = usePlaygroundPrefill()
|
|
351
|
+
|
|
352
|
+
// Playground navigation context for tab navigation
|
|
353
|
+
const { navigateAndHighlight, resetNavigation } = usePlaygroundNavigation()
|
|
354
|
+
|
|
355
|
+
// Mode context for switching modes
|
|
356
|
+
const { switchToDocs } = useModeContext()
|
|
357
|
+
|
|
358
|
+
// Ref for the scrollable content area
|
|
359
|
+
const contentRef = useRef<HTMLDivElement>(null)
|
|
360
|
+
|
|
361
|
+
// Update URL hash without triggering navigation
|
|
362
|
+
// Format: #tab or #tab/type/id (e.g., #api-reference, #api-reference/endpoint/123, #guides/page/quickstart)
|
|
363
|
+
const updateUrlHash = useCallback((hash: string, tab?: string) => {
|
|
364
|
+
const currentTab = tab || activeTab
|
|
365
|
+
let newUrl: string
|
|
366
|
+
|
|
367
|
+
if (!hash) {
|
|
368
|
+
// Just the tab
|
|
369
|
+
newUrl = `#${currentTab}`
|
|
370
|
+
} else {
|
|
371
|
+
// Tab + path
|
|
372
|
+
newUrl = `#${currentTab}/${hash}`
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
window.history.pushState(null, '', newUrl)
|
|
376
|
+
}, [activeTab])
|
|
377
|
+
|
|
378
|
+
// Navigate to hash - used for initial load and popstate
|
|
379
|
+
// Parse hash format: #tab or #tab/type/id
|
|
380
|
+
const parseHash = useCallback((hash: string) => {
|
|
381
|
+
if (!hash) return { tab: null, type: null, id: null }
|
|
382
|
+
|
|
383
|
+
const parts = hash.split('/')
|
|
384
|
+
const tab = parts[0] || null
|
|
385
|
+
const type = parts[1] || null
|
|
386
|
+
const id = parts.slice(2).join('/') || null // Rejoin in case id has slashes
|
|
387
|
+
|
|
388
|
+
return { tab, type, id }
|
|
389
|
+
}, [])
|
|
390
|
+
|
|
391
|
+
const navigateToHash = useCallback((collectionData: BrainfishCollection) => {
|
|
392
|
+
const hash = window.location.hash.slice(1) // Remove #
|
|
393
|
+
|
|
394
|
+
console.log('[Docs] Navigating to hash:', hash)
|
|
395
|
+
|
|
396
|
+
if (!hash) {
|
|
397
|
+
// No hash - show Introduction by default
|
|
398
|
+
setSelectedRequest(null)
|
|
399
|
+
setSelectedDocSection(null)
|
|
400
|
+
setSelectedDocPage(null)
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Notes mode is handled by ModeContext, just clear API selection
|
|
405
|
+
if (hash === 'notes' || hash.startsWith('notes/')) {
|
|
406
|
+
console.log('[Docs] Notes mode detected, clearing API selection')
|
|
407
|
+
setSelectedRequest(null)
|
|
408
|
+
setSelectedDocSection(null)
|
|
409
|
+
setSelectedDocPage(null)
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Parse new format: #tab/type/id
|
|
414
|
+
const { tab, type, id } = parseHash(hash)
|
|
415
|
+
|
|
416
|
+
// Set the active tab if specified
|
|
417
|
+
if (tab) {
|
|
418
|
+
setActiveTab(tab)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Handle legacy format (endpoint/xxx, page/xxx, doc/xxx without tab prefix)
|
|
422
|
+
const legacyType = hash.startsWith('endpoint/') ? 'endpoint'
|
|
423
|
+
: hash.startsWith('page/') ? 'page'
|
|
424
|
+
: hash.startsWith('doc/') ? 'doc'
|
|
425
|
+
: null
|
|
426
|
+
|
|
427
|
+
const actualType = type || legacyType
|
|
428
|
+
const actualId = id || (legacyType ? hash.replace(`${legacyType}/`, '') : null)
|
|
429
|
+
|
|
430
|
+
if (actualType === 'endpoint' && actualId) {
|
|
431
|
+
console.log('[Docs] Looking for endpoint:', actualId)
|
|
432
|
+
const request = findRequestById(collectionData, actualId)
|
|
433
|
+
console.log('[Docs] Found request:', request?.name || 'NOT FOUND')
|
|
434
|
+
if (request) {
|
|
435
|
+
setSelectedRequest(request)
|
|
436
|
+
setSelectedDocSection(null)
|
|
437
|
+
setSelectedDocPage(null)
|
|
438
|
+
} else {
|
|
439
|
+
// Endpoint not found - clear selection
|
|
440
|
+
setSelectedRequest(null)
|
|
441
|
+
setSelectedDocSection(null)
|
|
442
|
+
setSelectedDocPage(null)
|
|
443
|
+
}
|
|
444
|
+
} else if (actualType === 'page' && actualId) {
|
|
445
|
+
setSelectedDocPage(actualId)
|
|
446
|
+
setSelectedRequest(null)
|
|
447
|
+
setSelectedDocSection(null)
|
|
448
|
+
} else if (actualType === 'doc' && actualId) {
|
|
449
|
+
setSelectedDocSection(actualId)
|
|
450
|
+
setSelectedRequest(null)
|
|
451
|
+
setSelectedDocPage(null)
|
|
452
|
+
|
|
453
|
+
// Scroll to section after DOM update
|
|
454
|
+
setTimeout(() => {
|
|
455
|
+
const element = document.getElementById(actualId)
|
|
456
|
+
if (element) {
|
|
457
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
458
|
+
}
|
|
459
|
+
}, 100)
|
|
460
|
+
} else if (tab && !type) {
|
|
461
|
+
// Just a tab, no specific content - show default for that tab
|
|
462
|
+
setSelectedRequest(null)
|
|
463
|
+
setSelectedDocSection(null)
|
|
464
|
+
setSelectedDocPage(null)
|
|
465
|
+
}
|
|
466
|
+
}, [parseHash])
|
|
467
|
+
|
|
468
|
+
const handleSelectRequest = useCallback((request: BrainfishRESTRequest) => {
|
|
469
|
+
setSelectedRequest(request)
|
|
470
|
+
setSelectedDocSection(null)
|
|
471
|
+
setSelectedDocPage(null)
|
|
472
|
+
updateUrlHash(`endpoint/${request.id}`)
|
|
473
|
+
// Reset tab navigation so the new endpoint can determine its default tab
|
|
474
|
+
resetNavigation()
|
|
475
|
+
// Switch to Docs mode to show endpoint documentation first
|
|
476
|
+
switchToDocs()
|
|
477
|
+
}, [updateUrlHash, resetNavigation, switchToDocs])
|
|
478
|
+
|
|
479
|
+
const handleSelectDocumentation = useCallback((headingId: string) => {
|
|
480
|
+
const isIntro = headingId === 'introduction'
|
|
481
|
+
setSelectedDocSection(isIntro ? null : headingId)
|
|
482
|
+
setSelectedRequest(null)
|
|
483
|
+
setSelectedDocPage(null)
|
|
484
|
+
|
|
485
|
+
updateUrlHash(isIntro ? '' : `doc/${headingId}`)
|
|
486
|
+
|
|
487
|
+
// Switch to Docs mode when selecting documentation
|
|
488
|
+
switchToDocs()
|
|
489
|
+
|
|
490
|
+
setTimeout(() => {
|
|
491
|
+
if (isIntro) {
|
|
492
|
+
contentRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
|
|
493
|
+
} else {
|
|
494
|
+
const element = document.getElementById(headingId)
|
|
495
|
+
if (element) {
|
|
496
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}, 50)
|
|
500
|
+
}, [updateUrlHash, switchToDocs])
|
|
501
|
+
|
|
502
|
+
const handleSelectDocPage = useCallback((slug: string) => {
|
|
503
|
+
// Find which tab this doc page belongs to
|
|
504
|
+
let targetTab = activeTab
|
|
505
|
+
let releaseSlug: string | null = null
|
|
506
|
+
let sectionId: string | null = null
|
|
507
|
+
|
|
508
|
+
// Parse section from slug (e.g., "essentials/markdown#headings" -> slug: "essentials/markdown", section: "headings")
|
|
509
|
+
let pageSlug = slug
|
|
510
|
+
if (slug.includes('#')) {
|
|
511
|
+
const [pagePart, sectionPart] = slug.split('#')
|
|
512
|
+
pageSlug = pagePart
|
|
513
|
+
sectionId = sectionPart
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Special handling for changelog pages
|
|
517
|
+
if (pageSlug.startsWith('changelog/') || pageSlug === 'changelog') {
|
|
518
|
+
// Find the changelog tab
|
|
519
|
+
const changelogTab = collection?.navigationTabs?.find(t => t.type === 'changelog')
|
|
520
|
+
if (changelogTab) {
|
|
521
|
+
targetTab = changelogTab.id
|
|
522
|
+
}
|
|
523
|
+
// Extract the release slug for scrolling (e.g., "changelog/v1.2.0" -> "v1.2.0")
|
|
524
|
+
if (pageSlug.startsWith('changelog/')) {
|
|
525
|
+
releaseSlug = pageSlug.replace('changelog/', '')
|
|
526
|
+
}
|
|
527
|
+
} else if (collection?.docGroups) {
|
|
528
|
+
for (const group of collection.docGroups) {
|
|
529
|
+
const hasPage = (pages: typeof group.pages): boolean => {
|
|
530
|
+
for (const page of pages) {
|
|
531
|
+
if (page.slug === pageSlug) return true
|
|
532
|
+
if (page.children && hasPage(page.children)) return true
|
|
533
|
+
}
|
|
534
|
+
return false
|
|
535
|
+
}
|
|
536
|
+
if (hasPage(group.pages)) {
|
|
537
|
+
// Extract tab from group ID (e.g., "group-guides-getting-started" -> "guides")
|
|
538
|
+
const tabPart = group.id.replace('group-', '').split('-')[0]
|
|
539
|
+
// Find the actual tab ID that matches
|
|
540
|
+
const matchingTab = collection.navigationTabs?.find(t =>
|
|
541
|
+
t.id === tabPart || t.id.startsWith(tabPart)
|
|
542
|
+
)
|
|
543
|
+
if (matchingTab) {
|
|
544
|
+
targetTab = matchingTab.id
|
|
545
|
+
}
|
|
546
|
+
break
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Switch to the correct tab
|
|
552
|
+
setActiveTab(targetTab)
|
|
553
|
+
setSelectedDocPage(pageSlug)
|
|
554
|
+
setSelectedRequest(null)
|
|
555
|
+
setSelectedDocSection(null)
|
|
556
|
+
updateUrlHash(`page/${pageSlug}`, targetTab)
|
|
557
|
+
switchToDocs()
|
|
558
|
+
|
|
559
|
+
// Scroll to specific release if navigating to a changelog entry
|
|
560
|
+
if (releaseSlug) {
|
|
561
|
+
// Wait for the changelog page to render, then scroll to the release
|
|
562
|
+
setTimeout(() => {
|
|
563
|
+
const releaseElement = document.getElementById(`release-${releaseSlug}`)
|
|
564
|
+
if (releaseElement) {
|
|
565
|
+
releaseElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
566
|
+
}
|
|
567
|
+
}, 600)
|
|
568
|
+
} else if (sectionId) {
|
|
569
|
+
// Scroll to section within the page after it renders
|
|
570
|
+
setTimeout(() => {
|
|
571
|
+
const sectionElement = document.getElementById(sectionId)
|
|
572
|
+
if (sectionElement) {
|
|
573
|
+
sectionElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
574
|
+
}
|
|
575
|
+
}, 300)
|
|
576
|
+
} else {
|
|
577
|
+
// Scroll to top of content when navigating to a new page
|
|
578
|
+
setTimeout(() => {
|
|
579
|
+
contentRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
|
|
580
|
+
}, 50)
|
|
581
|
+
}
|
|
582
|
+
}, [updateUrlHash, switchToDocs, collection, activeTab])
|
|
583
|
+
|
|
584
|
+
// Handle API version change
|
|
585
|
+
const handleApiVersionChange = useCallback((version: string) => {
|
|
586
|
+
if (version !== selectedApiVersion) {
|
|
587
|
+
setSelectedApiVersion(version)
|
|
588
|
+
// Clear selected request when switching versions as endpoints may differ
|
|
589
|
+
setSelectedRequest(null)
|
|
590
|
+
setSelectedDocSection(null)
|
|
591
|
+
}
|
|
592
|
+
}, [selectedApiVersion])
|
|
593
|
+
|
|
594
|
+
// Handle tab change from header
|
|
595
|
+
const handleTabChange = useCallback((tabId: string) => {
|
|
596
|
+
setActiveTab(tabId)
|
|
597
|
+
|
|
598
|
+
// Reset mode to docs when switching tabs (exit sandbox/api-client mode)
|
|
599
|
+
switchToDocs()
|
|
600
|
+
|
|
601
|
+
if (tabId === 'api-reference') {
|
|
602
|
+
// Switch to API Reference - clear doc page and show introduction (no endpoint selected)
|
|
603
|
+
setSelectedDocPage(null)
|
|
604
|
+
setSelectedRequest(null)
|
|
605
|
+
setSelectedDocSection(null)
|
|
606
|
+
updateUrlHash('', tabId)
|
|
607
|
+
} else if (tabId === 'changelog') {
|
|
608
|
+
// Switch to Changelog tab
|
|
609
|
+
setSelectedDocPage(null)
|
|
610
|
+
setSelectedRequest(null)
|
|
611
|
+
setSelectedDocSection(null)
|
|
612
|
+
updateUrlHash('', tabId)
|
|
613
|
+
} else {
|
|
614
|
+
// Switch to a doc group tab - find and select the first page in that tab
|
|
615
|
+
setSelectedRequest(null)
|
|
616
|
+
setSelectedDocSection(null)
|
|
617
|
+
|
|
618
|
+
// Find the first doc group for this tab and select its first page
|
|
619
|
+
if (collection?.docGroups) {
|
|
620
|
+
const tabDocGroup = collection.docGroups.find(g => {
|
|
621
|
+
const groupTabPart = g.id.replace('group-', '').split('-')[0]
|
|
622
|
+
return groupTabPart === tabId
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
if (tabDocGroup && tabDocGroup.pages.length > 0) {
|
|
626
|
+
const firstPage = tabDocGroup.pages[0]
|
|
627
|
+
setSelectedDocPage(firstPage.slug)
|
|
628
|
+
updateUrlHash(`page/${firstPage.slug}`, tabId)
|
|
629
|
+
switchToDocs()
|
|
630
|
+
} else {
|
|
631
|
+
setSelectedDocPage(null)
|
|
632
|
+
updateUrlHash('', tabId)
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}, [collection, updateUrlHash, switchToDocs])
|
|
637
|
+
|
|
638
|
+
// Handler for agent navigation
|
|
639
|
+
const handleAgentNavigate = useCallback((endpointId: string) => {
|
|
640
|
+
if (!collection) return
|
|
641
|
+
const request = findRequestById(collection, endpointId)
|
|
642
|
+
if (request) {
|
|
643
|
+
// Set the tab to API Reference first
|
|
644
|
+
setActiveTab('api-reference')
|
|
645
|
+
// Then set the selected request
|
|
646
|
+
setSelectedRequest(request)
|
|
647
|
+
setSelectedDocSection(null)
|
|
648
|
+
setSelectedDocPage(null)
|
|
649
|
+
updateUrlHash(`endpoint/${endpointId}`, 'api-reference')
|
|
650
|
+
// Reset tab navigation so the new endpoint can determine its default tab
|
|
651
|
+
resetNavigation()
|
|
652
|
+
}
|
|
653
|
+
}, [collection, updateUrlHash, resetNavigation])
|
|
654
|
+
|
|
655
|
+
// Handler for agent prefilling parameters
|
|
656
|
+
const handleAgentPrefill = useCallback((data: PrefillData) => {
|
|
657
|
+
console.log('[Agent] Prefill requested:', data)
|
|
658
|
+
setPrefill(data)
|
|
659
|
+
}, [setPrefill])
|
|
660
|
+
|
|
661
|
+
// State for debug context - used to trigger agent debugging
|
|
662
|
+
const [debugContext, setDebugContext] = useState<DebugContext | null>(null)
|
|
663
|
+
|
|
664
|
+
// Handler for debug requests from playground
|
|
665
|
+
const handleDebugRequest = useCallback((context: DebugContext) => {
|
|
666
|
+
setDebugContext(context)
|
|
667
|
+
}, [])
|
|
668
|
+
|
|
669
|
+
// Clear debug context after it's been used
|
|
670
|
+
const clearDebugContext = useCallback(() => {
|
|
671
|
+
setDebugContext(null)
|
|
672
|
+
}, [])
|
|
673
|
+
|
|
674
|
+
// State for explain context - used to trigger agent explanation
|
|
675
|
+
const [explainContext, setExplainContext] = useState<DebugContext | null>(null)
|
|
676
|
+
|
|
677
|
+
// Handler for explain requests from playground
|
|
678
|
+
const handleExplainRequest = useCallback((context: DebugContext) => {
|
|
679
|
+
setExplainContext(context)
|
|
680
|
+
}, [])
|
|
681
|
+
|
|
682
|
+
// Clear explain context after it's been used
|
|
683
|
+
const clearExplainContext = useCallback(() => {
|
|
684
|
+
setExplainContext(null)
|
|
685
|
+
}, [])
|
|
686
|
+
|
|
687
|
+
// Handle browser back/forward
|
|
688
|
+
useEffect(() => {
|
|
689
|
+
if (!collection) return
|
|
690
|
+
|
|
691
|
+
const handlePopState = () => {
|
|
692
|
+
navigateToHash(collection)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
window.addEventListener('popstate', handlePopState)
|
|
696
|
+
return () => window.removeEventListener('popstate', handlePopState)
|
|
697
|
+
}, [collection, navigateToHash])
|
|
698
|
+
|
|
699
|
+
// Dynamically set favicon from docs.json config
|
|
700
|
+
useEffect(() => {
|
|
701
|
+
if (!collection?.docsFavicon) return
|
|
702
|
+
|
|
703
|
+
const faviconPath = collection.docsFavicon
|
|
704
|
+
|
|
705
|
+
// Update or create link elements for favicon
|
|
706
|
+
const updateFaviconLink = (rel: string, href: string) => {
|
|
707
|
+
let link = document.querySelector(`link[rel="${rel}"]`) as HTMLLinkElement
|
|
708
|
+
if (!link) {
|
|
709
|
+
link = document.createElement('link')
|
|
710
|
+
link.rel = rel
|
|
711
|
+
document.head.appendChild(link)
|
|
712
|
+
}
|
|
713
|
+
link.href = href
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Determine the type based on file extension
|
|
717
|
+
const ext = faviconPath.split('.').pop()?.toLowerCase()
|
|
718
|
+
const type = ext === 'svg' ? 'image/svg+xml'
|
|
719
|
+
: ext === 'png' ? 'image/png'
|
|
720
|
+
: ext === 'ico' ? 'image/x-icon'
|
|
721
|
+
: 'image/png'
|
|
722
|
+
|
|
723
|
+
// Update the favicon link with type
|
|
724
|
+
let iconLink = document.querySelector('link[rel="icon"]') as HTMLLinkElement
|
|
725
|
+
if (!iconLink) {
|
|
726
|
+
iconLink = document.createElement('link')
|
|
727
|
+
iconLink.rel = 'icon'
|
|
728
|
+
document.head.appendChild(iconLink)
|
|
729
|
+
}
|
|
730
|
+
iconLink.type = type
|
|
731
|
+
iconLink.href = faviconPath
|
|
732
|
+
|
|
733
|
+
// Also update shortcut icon and apple-touch-icon
|
|
734
|
+
updateFaviconLink('shortcut icon', faviconPath)
|
|
735
|
+
updateFaviconLink('apple-touch-icon', faviconPath)
|
|
736
|
+
|
|
737
|
+
console.log('[Docs] Set favicon to:', faviconPath)
|
|
738
|
+
}, [collection?.docsFavicon])
|
|
739
|
+
|
|
740
|
+
useEffect(() => {
|
|
741
|
+
async function fetchCollection() {
|
|
742
|
+
try {
|
|
743
|
+
// Only show full loading on initial load
|
|
744
|
+
const isInitialLoad = !collection
|
|
745
|
+
if (isInitialLoad) {
|
|
746
|
+
setLoading(true)
|
|
747
|
+
} else {
|
|
748
|
+
setIsVersionLoading(true)
|
|
749
|
+
}
|
|
750
|
+
setError(null)
|
|
751
|
+
|
|
752
|
+
// Build URL with version param if selected
|
|
753
|
+
const url = selectedApiVersion
|
|
754
|
+
? `/api/collections?version=${encodeURIComponent(selectedApiVersion)}`
|
|
755
|
+
: '/api/collections'
|
|
756
|
+
|
|
757
|
+
const response = await fetch(url)
|
|
758
|
+
|
|
759
|
+
if (!response.ok) {
|
|
760
|
+
throw new Error(`Failed to fetch collection: ${response.status}`)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const data = await response.json()
|
|
764
|
+
|
|
765
|
+
console.log('[Docs] Collection data:', JSON.stringify({
|
|
766
|
+
hasData: !!data,
|
|
767
|
+
name: data?.name,
|
|
768
|
+
requestsCount: data?.requests?.length || 0,
|
|
769
|
+
foldersCount: data?.folders?.length || 0,
|
|
770
|
+
apiVersions: data?.apiVersions?.length || 0,
|
|
771
|
+
selectedVersion: data?.selectedApiVersion,
|
|
772
|
+
}, null, 2))
|
|
773
|
+
|
|
774
|
+
setCollection(data)
|
|
775
|
+
|
|
776
|
+
// Set initial API version if not already set
|
|
777
|
+
if (!selectedApiVersion && data?.selectedApiVersion) {
|
|
778
|
+
setSelectedApiVersion(data.selectedApiVersion)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Only run initial navigation logic on first load, not on version changes
|
|
782
|
+
if (isInitialLoad) {
|
|
783
|
+
// Set initial active tab from config (first tab) and select first item
|
|
784
|
+
let initialTabId = 'api-reference'
|
|
785
|
+
if (data?.navigationTabs && data.navigationTabs.length > 0) {
|
|
786
|
+
const sortedTabs = [...data.navigationTabs].sort((a, b) => a.order - b.order)
|
|
787
|
+
initialTabId = sortedTabs[0].id
|
|
788
|
+
setActiveTab(initialTabId)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Handle initial hash navigation after collection loads
|
|
792
|
+
if (data) {
|
|
793
|
+
// Use setTimeout to ensure state is set before navigation
|
|
794
|
+
setTimeout(() => {
|
|
795
|
+
const hash = window.location.hash.slice(1)
|
|
796
|
+
console.log('[Docs] Initial hash:', hash)
|
|
797
|
+
|
|
798
|
+
if (!hash) {
|
|
799
|
+
// No hash - set URL to initial tab
|
|
800
|
+
updateUrlHash('', initialTabId)
|
|
801
|
+
setSelectedRequest(null)
|
|
802
|
+
setSelectedDocSection(null)
|
|
803
|
+
|
|
804
|
+
if (initialTabId === 'api-reference') {
|
|
805
|
+
// API Reference tab - show introduction
|
|
806
|
+
setSelectedDocPage(null)
|
|
807
|
+
switchToDocs()
|
|
808
|
+
} else {
|
|
809
|
+
// Doc group tab - select first page
|
|
810
|
+
const tabDocGroup = data.docGroups?.find((g: BrainfishDocGroup) => {
|
|
811
|
+
const groupTabPart = g.id.replace('group-', '').split('-')[0]
|
|
812
|
+
return groupTabPart === initialTabId
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
if (tabDocGroup && tabDocGroup.pages.length > 0) {
|
|
816
|
+
const firstPage = tabDocGroup.pages[0]
|
|
817
|
+
setSelectedDocPage(firstPage.slug)
|
|
818
|
+
updateUrlHash(`page/${firstPage.slug}`, initialTabId)
|
|
819
|
+
switchToDocs()
|
|
820
|
+
} else {
|
|
821
|
+
setSelectedDocPage(null)
|
|
822
|
+
switchToDocs()
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
} else if (hash === 'notes' || hash.startsWith('notes/')) {
|
|
826
|
+
// Notes mode - handled by ModeContext, just clear API selection
|
|
827
|
+
console.log('[Docs] Initial hash is notes mode')
|
|
828
|
+
setSelectedRequest(null)
|
|
829
|
+
setSelectedDocSection(null)
|
|
830
|
+
} else {
|
|
831
|
+
// Parse the hash to get tab and content info
|
|
832
|
+
// Format: #tab or #tab/type/id (e.g., #api-reference/endpoint/123)
|
|
833
|
+
const parts = hash.split('/')
|
|
834
|
+
const hashTab = parts[0]
|
|
835
|
+
const hashType = parts[1]
|
|
836
|
+
const hashId = parts.slice(2).join('/')
|
|
837
|
+
|
|
838
|
+
// Set the tab from the hash
|
|
839
|
+
if (hashTab && data.navigationTabs?.some((t: NavigationTab) => t.id === hashTab)) {
|
|
840
|
+
setActiveTab(hashTab)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Handle legacy format (endpoint/xxx without tab prefix)
|
|
844
|
+
const isLegacyFormat = hash.startsWith('endpoint/') || hash.startsWith('page/') || hash.startsWith('doc/')
|
|
845
|
+
const actualType = isLegacyFormat ? parts[0] : hashType
|
|
846
|
+
const actualId = isLegacyFormat ? parts.slice(1).join('/') : hashId
|
|
847
|
+
|
|
848
|
+
if (actualType === 'endpoint' && actualId) {
|
|
849
|
+
console.log('[Docs] Looking for endpoint on load:', actualId)
|
|
850
|
+
const request = findRequestById(data, actualId)
|
|
851
|
+
console.log('[Docs] Found:', request?.name || 'NOT FOUND')
|
|
852
|
+
if (request) {
|
|
853
|
+
setSelectedRequest(request)
|
|
854
|
+
setSelectedDocSection(null)
|
|
855
|
+
setSelectedDocPage(null)
|
|
856
|
+
switchToDocs()
|
|
857
|
+
} else {
|
|
858
|
+
setSelectedRequest(null)
|
|
859
|
+
setSelectedDocSection(null)
|
|
860
|
+
setSelectedDocPage(null)
|
|
861
|
+
switchToDocs()
|
|
862
|
+
}
|
|
863
|
+
} else if (actualType === 'page' && actualId) {
|
|
864
|
+
setSelectedDocPage(actualId)
|
|
865
|
+
setSelectedRequest(null)
|
|
866
|
+
setSelectedDocSection(null)
|
|
867
|
+
switchToDocs()
|
|
868
|
+
} else if (actualType === 'doc' && actualId) {
|
|
869
|
+
setSelectedDocSection(actualId)
|
|
870
|
+
setSelectedRequest(null)
|
|
871
|
+
setSelectedDocPage(null)
|
|
872
|
+
switchToDocs()
|
|
873
|
+
setTimeout(() => {
|
|
874
|
+
const element = document.getElementById(actualId)
|
|
875
|
+
if (element) {
|
|
876
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
877
|
+
}
|
|
878
|
+
}, 100)
|
|
879
|
+
} else if (hashTab && !hashType) {
|
|
880
|
+
// Just a tab, show its default content
|
|
881
|
+
setSelectedRequest(null)
|
|
882
|
+
setSelectedDocSection(null)
|
|
883
|
+
setSelectedDocPage(null)
|
|
884
|
+
switchToDocs()
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}, 0)
|
|
888
|
+
}
|
|
889
|
+
} else {
|
|
890
|
+
// Version change - clear selected endpoint as it may not exist in the new version
|
|
891
|
+
setSelectedRequest(null)
|
|
892
|
+
setSelectedDocSection(null)
|
|
893
|
+
}
|
|
894
|
+
} catch (err) {
|
|
895
|
+
console.error('Error fetching collection:', err)
|
|
896
|
+
setError(err instanceof Error ? err.message : 'Failed to load API documentation')
|
|
897
|
+
} finally {
|
|
898
|
+
setLoading(false)
|
|
899
|
+
setIsVersionLoading(false)
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
fetchCollection()
|
|
904
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
905
|
+
}, [switchToDocs, selectedApiVersion])
|
|
906
|
+
|
|
907
|
+
// Only show full-page loading on initial load
|
|
908
|
+
if (loading && !collection) {
|
|
909
|
+
return (
|
|
910
|
+
<div className="flex items-center justify-center h-screen">
|
|
911
|
+
<div className="text-center">
|
|
912
|
+
<Spinner size={32} className="text-muted-foreground animate-spin mx-auto mb-3" />
|
|
913
|
+
<p className="text-sm text-muted-foreground">Loading API documentation...</p>
|
|
914
|
+
</div>
|
|
915
|
+
</div>
|
|
916
|
+
)
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (error) {
|
|
920
|
+
return (
|
|
921
|
+
<div className="flex items-center justify-center h-screen">
|
|
922
|
+
<div className="text-center">
|
|
923
|
+
<p className="text-destructive text-lg mb-2">Error loading documentation</p>
|
|
924
|
+
<p className="text-muted-foreground">{error}</p>
|
|
925
|
+
</div>
|
|
926
|
+
</div>
|
|
927
|
+
)
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (!collection) {
|
|
931
|
+
return (
|
|
932
|
+
<div className="flex items-center justify-center h-screen">
|
|
933
|
+
<div className="text-center max-w-md">
|
|
934
|
+
<p className="text-muted-foreground mb-2">No API documentation available</p>
|
|
935
|
+
<p className="text-sm text-muted-foreground">
|
|
936
|
+
Please configure your Brainfish API credentials in your environment variables.
|
|
937
|
+
</p>
|
|
938
|
+
</div>
|
|
939
|
+
</div>
|
|
940
|
+
)
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Only show "configure credentials" if there are no endpoints AND no doc groups
|
|
944
|
+
// Multi-tenant docs may have only doc groups without API endpoints
|
|
945
|
+
const hasDocGroups = collection.docGroups && collection.docGroups.length > 0
|
|
946
|
+
const hasEndpoints = collection.requests.length > 0 || collection.folders.length > 0
|
|
947
|
+
|
|
948
|
+
if (!hasEndpoints && !hasDocGroups) {
|
|
949
|
+
return (
|
|
950
|
+
<div className="flex items-center justify-center h-screen">
|
|
951
|
+
<div className="text-center max-w-lg p-8">
|
|
952
|
+
<h2 className="text-2xl font-semibold mb-2">{collection.name || 'Documentation'}</h2>
|
|
953
|
+
<p className="text-muted-foreground mb-6">
|
|
954
|
+
{collection.description || 'No documentation content available yet.'}
|
|
955
|
+
</p>
|
|
956
|
+
<div className="bg-muted/50 rounded-lg p-5 text-left text-sm space-y-4">
|
|
957
|
+
<p className="font-medium">Get started with DevDoc:</p>
|
|
958
|
+
<div className="space-y-3">
|
|
959
|
+
<div>
|
|
960
|
+
<p className="text-muted-foreground text-xs mb-1">Create a new project</p>
|
|
961
|
+
<code className="block bg-background px-3 py-2 rounded-md font-mono text-xs">
|
|
962
|
+
npx create-devdoc-doc my-docs
|
|
963
|
+
</code>
|
|
964
|
+
</div>
|
|
965
|
+
<div>
|
|
966
|
+
<p className="text-muted-foreground text-xs mb-1">Start local development</p>
|
|
967
|
+
<code className="block bg-background px-3 py-2 rounded-md font-mono text-xs">
|
|
968
|
+
npx devdoc dev
|
|
969
|
+
</code>
|
|
970
|
+
</div>
|
|
971
|
+
<div>
|
|
972
|
+
<p className="text-muted-foreground text-xs mb-1">Deploy to production</p>
|
|
973
|
+
<code className="block bg-background px-3 py-2 rounded-md font-mono text-xs">
|
|
974
|
+
npx devdoc deploy
|
|
975
|
+
</code>
|
|
976
|
+
</div>
|
|
977
|
+
</div>
|
|
978
|
+
<a
|
|
979
|
+
href="https://devdoc.sh"
|
|
980
|
+
target="_blank"
|
|
981
|
+
rel="noopener noreferrer"
|
|
982
|
+
className="inline-flex items-center gap-1.5 text-primary hover:underline text-sm font-medium"
|
|
983
|
+
>
|
|
984
|
+
Learn more at devdoc.sh
|
|
985
|
+
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
986
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
987
|
+
</svg>
|
|
988
|
+
</a>
|
|
989
|
+
</div>
|
|
990
|
+
</div>
|
|
991
|
+
</div>
|
|
992
|
+
)
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return (
|
|
996
|
+
<NavigationProvider collection={collection} onSelectRequest={handleSelectRequest}>
|
|
997
|
+
<DocsWithMode
|
|
998
|
+
collection={collection}
|
|
999
|
+
selectedRequest={selectedRequest}
|
|
1000
|
+
selectedDocSection={selectedDocSection}
|
|
1001
|
+
selectedDocPage={selectedDocPage}
|
|
1002
|
+
activeTab={activeTab}
|
|
1003
|
+
onTabChange={handleTabChange}
|
|
1004
|
+
contentRef={contentRef}
|
|
1005
|
+
showAuthModal={showAuthModal}
|
|
1006
|
+
setShowAuthModal={setShowAuthModal}
|
|
1007
|
+
handleSelectRequest={handleSelectRequest}
|
|
1008
|
+
handleSelectDocumentation={handleSelectDocumentation}
|
|
1009
|
+
handleSelectDocPage={handleSelectDocPage}
|
|
1010
|
+
handleDebugRequest={handleDebugRequest}
|
|
1011
|
+
handleExplainRequest={handleExplainRequest}
|
|
1012
|
+
handleAgentNavigate={handleAgentNavigate}
|
|
1013
|
+
handleAgentPrefill={handleAgentPrefill}
|
|
1014
|
+
debugContext={debugContext}
|
|
1015
|
+
clearDebugContext={clearDebugContext}
|
|
1016
|
+
explainContext={explainContext}
|
|
1017
|
+
clearExplainContext={clearExplainContext}
|
|
1018
|
+
navigateAndHighlight={navigateAndHighlight}
|
|
1019
|
+
selectedApiVersion={selectedApiVersion}
|
|
1020
|
+
handleApiVersionChange={handleApiVersionChange}
|
|
1021
|
+
isVersionLoading={isVersionLoading}
|
|
1022
|
+
/>
|
|
1023
|
+
</NavigationProvider>
|
|
1024
|
+
)
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Mode-aware rendering component
|
|
1028
|
+
interface DocsWithModeProps {
|
|
1029
|
+
collection: CollectionWithSummary
|
|
1030
|
+
selectedRequest: BrainfishRESTRequest | null
|
|
1031
|
+
selectedDocSection: string | null
|
|
1032
|
+
selectedDocPage: string | null
|
|
1033
|
+
activeTab: string
|
|
1034
|
+
onTabChange: (tabId: string) => void
|
|
1035
|
+
contentRef: React.RefObject<HTMLDivElement | null>
|
|
1036
|
+
showAuthModal: boolean
|
|
1037
|
+
setShowAuthModal: (show: boolean) => void
|
|
1038
|
+
handleSelectRequest: (request: BrainfishRESTRequest) => void
|
|
1039
|
+
handleSelectDocumentation: (headingId: string) => void
|
|
1040
|
+
handleSelectDocPage: (slug: string) => void
|
|
1041
|
+
handleDebugRequest: (context: DebugContext) => void
|
|
1042
|
+
handleExplainRequest: (context: DebugContext) => void
|
|
1043
|
+
handleAgentNavigate: (endpointId: string) => void
|
|
1044
|
+
handleAgentPrefill: (data: PrefillData) => void
|
|
1045
|
+
debugContext: DebugContext | null
|
|
1046
|
+
clearDebugContext: () => void
|
|
1047
|
+
explainContext: DebugContext | null
|
|
1048
|
+
clearExplainContext: () => void
|
|
1049
|
+
navigateAndHighlight: (tab: PlaygroundTab, field?: HighlightField, showError?: boolean) => void
|
|
1050
|
+
selectedApiVersion: string | null
|
|
1051
|
+
handleApiVersionChange: (version: string) => void
|
|
1052
|
+
isVersionLoading: boolean
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Mode toggle tabs - switches between Docs, API Client, and Notes
|
|
1056
|
+
function ModeToggleTabs({ hasEndpoint }: { hasEndpoint: boolean }) {
|
|
1057
|
+
const { mode, switchToDocs, switchToApiClient, switchToNotes } = useModeContext()
|
|
1058
|
+
|
|
1059
|
+
return (
|
|
1060
|
+
<div className="flex items-center gap-1 p-1 rounded-lg bg-muted/50 shrink-0">
|
|
1061
|
+
<Button
|
|
1062
|
+
variant={mode === 'docs' ? 'secondary' : 'ghost'}
|
|
1063
|
+
size="sm"
|
|
1064
|
+
onClick={() => switchToDocs()}
|
|
1065
|
+
className={cn(
|
|
1066
|
+
"text-xs h-7 px-2 sm:px-3 gap-1.5",
|
|
1067
|
+
mode === 'docs' ? 'bg-background shadow-sm' : 'text-muted-foreground hover:text-foreground'
|
|
1068
|
+
)}
|
|
1069
|
+
>
|
|
1070
|
+
<Book className="h-3.5 w-3.5" weight="bold" />
|
|
1071
|
+
<span className="hidden xs:inline">Docs</span>
|
|
1072
|
+
</Button>
|
|
1073
|
+
{hasEndpoint && (
|
|
1074
|
+
<Button
|
|
1075
|
+
variant={mode === 'api_client' ? 'secondary' : 'ghost'}
|
|
1076
|
+
size="sm"
|
|
1077
|
+
onClick={() => switchToApiClient()}
|
|
1078
|
+
className={cn(
|
|
1079
|
+
"text-xs h-7 px-2 sm:px-3 gap-1.5",
|
|
1080
|
+
mode === 'api_client' ? 'bg-background shadow-sm' : 'text-muted-foreground hover:text-foreground'
|
|
1081
|
+
)}
|
|
1082
|
+
>
|
|
1083
|
+
<TestTube className="h-3.5 w-3.5" weight="bold" />
|
|
1084
|
+
<span className="hidden xs:inline">API Client</span>
|
|
1085
|
+
</Button>
|
|
1086
|
+
)}
|
|
1087
|
+
<Button
|
|
1088
|
+
variant={mode === 'notes' ? 'secondary' : 'ghost'}
|
|
1089
|
+
size="sm"
|
|
1090
|
+
onClick={() => switchToNotes()}
|
|
1091
|
+
className={cn(
|
|
1092
|
+
"text-xs h-7 px-2 sm:px-3 gap-1.5",
|
|
1093
|
+
mode === 'notes' ? 'bg-background shadow-sm' : 'text-muted-foreground hover:text-foreground'
|
|
1094
|
+
)}
|
|
1095
|
+
>
|
|
1096
|
+
<Code className="h-3.5 w-3.5" weight="bold" />
|
|
1097
|
+
<span className="hidden xs:inline">Sandbox</span>
|
|
1098
|
+
</Button>
|
|
1099
|
+
</div>
|
|
1100
|
+
)
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function DocsWithMode({
|
|
1104
|
+
collection,
|
|
1105
|
+
selectedRequest,
|
|
1106
|
+
selectedDocSection,
|
|
1107
|
+
selectedDocPage,
|
|
1108
|
+
activeTab,
|
|
1109
|
+
onTabChange,
|
|
1110
|
+
contentRef,
|
|
1111
|
+
showAuthModal,
|
|
1112
|
+
setShowAuthModal,
|
|
1113
|
+
handleSelectRequest,
|
|
1114
|
+
handleSelectDocumentation,
|
|
1115
|
+
handleSelectDocPage,
|
|
1116
|
+
handleDebugRequest,
|
|
1117
|
+
handleExplainRequest,
|
|
1118
|
+
handleAgentNavigate,
|
|
1119
|
+
handleAgentPrefill,
|
|
1120
|
+
debugContext,
|
|
1121
|
+
clearDebugContext,
|
|
1122
|
+
explainContext,
|
|
1123
|
+
clearExplainContext,
|
|
1124
|
+
navigateAndHighlight,
|
|
1125
|
+
selectedApiVersion,
|
|
1126
|
+
handleApiVersionChange,
|
|
1127
|
+
isVersionLoading,
|
|
1128
|
+
}: DocsWithModeProps) {
|
|
1129
|
+
const { mode, switchToDocs } = useModeContext()
|
|
1130
|
+
const { setTheme } = useTheme()
|
|
1131
|
+
|
|
1132
|
+
// GraphQL state
|
|
1133
|
+
const [graphqlOperations, setGraphqlOperations] = useState<GraphQLOperationItem[]>([])
|
|
1134
|
+
const [graphqlCollection, setGraphqlCollection] = useState<BrainfishCollection | null>(null)
|
|
1135
|
+
const [selectedGraphQLOperation, setSelectedGraphQLOperation] = useState<GraphQLOperationItem | null>(null)
|
|
1136
|
+
|
|
1137
|
+
// Get API spec URL from environment or collection
|
|
1138
|
+
const apiSpecUrl = process.env.NEXT_PUBLIC_OPENAPI_URL || collection.name || 'default'
|
|
1139
|
+
|
|
1140
|
+
// Check if there are endpoints
|
|
1141
|
+
const hasEndpoints = collection.folders.length > 0 || collection.requests.length > 0
|
|
1142
|
+
|
|
1143
|
+
// Search functionality
|
|
1144
|
+
const { isOpen: searchOpen, setIsOpen: setSearchOpen, openSearch, searchItems } = useSearch({
|
|
1145
|
+
requests: collection.requests,
|
|
1146
|
+
folders: collection.folders,
|
|
1147
|
+
docGroups: collection.docGroups,
|
|
1148
|
+
})
|
|
1149
|
+
|
|
1150
|
+
// Handle search item selection - use URL hash navigation
|
|
1151
|
+
const handleSearchSelect = useCallback((item: { id: string; type: string; href: string }) => {
|
|
1152
|
+
if (item.type === 'endpoint') {
|
|
1153
|
+
// Navigate to endpoint via hash - format: #api-reference/endpoint/{id}
|
|
1154
|
+
window.location.hash = `api-reference/endpoint/${item.id}`
|
|
1155
|
+
} else if (item.type === 'doc') {
|
|
1156
|
+
// Navigate to doc page via hash - the href already has the correct format
|
|
1157
|
+
// Extract tab and path from href (e.g., #guides/page/quickstart)
|
|
1158
|
+
const hashPath = item.href.replace('#', '')
|
|
1159
|
+
window.location.hash = hashPath
|
|
1160
|
+
}
|
|
1161
|
+
}, [])
|
|
1162
|
+
|
|
1163
|
+
// Wrap agent navigate to switch to Docs mode and navigate to endpoint
|
|
1164
|
+
const handleAgentNavigateWithModeSwitch = useCallback((endpointId: string) => {
|
|
1165
|
+
// Just navigate - handleAgentNavigate already sets the tab via setActiveTab
|
|
1166
|
+
handleAgentNavigate(endpointId)
|
|
1167
|
+
}, [handleAgentNavigate])
|
|
1168
|
+
|
|
1169
|
+
// Get the current content title for header
|
|
1170
|
+
// Find the active tab's config
|
|
1171
|
+
const activeTabConfig = collection.navigationTabs?.find(t => t.id === activeTab)
|
|
1172
|
+
const activeTabType = activeTabConfig?.type || 'docs'
|
|
1173
|
+
// Use the tab name for page header
|
|
1174
|
+
const activeTabTitle = activeTabConfig?.tab || 'Documentation'
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
// Filter doc groups for sidebar based on active tab
|
|
1178
|
+
// Group IDs are like "group-guides-getting-started", tab IDs are like "guides"
|
|
1179
|
+
const filteredDocGroups = collection.docGroups?.filter(g => {
|
|
1180
|
+
// Extract the tab part from the group ID (e.g., "guides" from "group-guides-getting-started")
|
|
1181
|
+
const groupTabPart = g.id.replace('group-', '').split('-')[0]
|
|
1182
|
+
return groupTabPart === activeTab
|
|
1183
|
+
}) || []
|
|
1184
|
+
|
|
1185
|
+
// Show endpoints in sidebar only when OpenAPI tab is active
|
|
1186
|
+
const showEndpoints = activeTabType === 'openapi'
|
|
1187
|
+
|
|
1188
|
+
// Show changelog when changelog tab is active
|
|
1189
|
+
const showChangelog = activeTabType === 'changelog'
|
|
1190
|
+
|
|
1191
|
+
// Show GraphQL playground when graphql tab is active
|
|
1192
|
+
const showGraphQL = activeTabType === 'graphql'
|
|
1193
|
+
const activeGraphQLSchemas = activeTabConfig?.graphqlSchemas || []
|
|
1194
|
+
|
|
1195
|
+
// Get the first schema path for stable dependency
|
|
1196
|
+
const schemaPath = activeGraphQLSchemas[0]?.schema
|
|
1197
|
+
const schemaEndpoint = activeGraphQLSchemas[0]?.endpoint
|
|
1198
|
+
|
|
1199
|
+
// Load GraphQL operations when graphql tab is active
|
|
1200
|
+
useEffect(() => {
|
|
1201
|
+
if (!showGraphQL || !schemaPath) {
|
|
1202
|
+
setGraphqlOperations([])
|
|
1203
|
+
setGraphqlCollection(null)
|
|
1204
|
+
return
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// Load and parse GraphQL schema
|
|
1208
|
+
const loadGraphQLSchema = async () => {
|
|
1209
|
+
try {
|
|
1210
|
+
const response = await fetch(`/api/schema?path=${encodeURIComponent(schemaPath)}`)
|
|
1211
|
+
if (!response.ok) return
|
|
1212
|
+
|
|
1213
|
+
const schemaContent = await response.text()
|
|
1214
|
+
const operations = parseGraphQLSchema(schemaContent)
|
|
1215
|
+
setGraphqlOperations(operations)
|
|
1216
|
+
|
|
1217
|
+
// Convert to BrainfishCollection for sidebar
|
|
1218
|
+
const gqlCollection = convertGraphQLToCollection(operations, schemaEndpoint || '')
|
|
1219
|
+
setGraphqlCollection(gqlCollection)
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
console.error('Failed to load GraphQL schema:', err)
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
loadGraphQLSchema()
|
|
1226
|
+
}, [showGraphQL, schemaPath, schemaEndpoint])
|
|
1227
|
+
|
|
1228
|
+
// Handle GraphQL operation selection from sidebar
|
|
1229
|
+
const handleSelectGraphQLOperation = useCallback((request: BrainfishRESTRequest) => {
|
|
1230
|
+
// Find the matching GraphQL operation
|
|
1231
|
+
const operation = graphqlOperations.find(op => op.id === request.id)
|
|
1232
|
+
if (operation) {
|
|
1233
|
+
setSelectedGraphQLOperation(operation)
|
|
1234
|
+
// Update URL hash
|
|
1235
|
+
window.history.pushState(null, '', `#${activeTab}/endpoint/${request.id}`)
|
|
1236
|
+
switchToDocs()
|
|
1237
|
+
}
|
|
1238
|
+
}, [graphqlOperations, activeTab, switchToDocs])
|
|
1239
|
+
|
|
1240
|
+
// Inject custom CSS and color variables
|
|
1241
|
+
useEffect(() => {
|
|
1242
|
+
// Create or update style element for custom CSS
|
|
1243
|
+
let styleEl = document.getElementById('docs-custom-css') as HTMLStyleElement | null
|
|
1244
|
+
if (!styleEl) {
|
|
1245
|
+
styleEl = document.createElement('style')
|
|
1246
|
+
styleEl.id = 'docs-custom-css'
|
|
1247
|
+
document.head.appendChild(styleEl)
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// Build CSS with color variables
|
|
1251
|
+
let cssContent = ''
|
|
1252
|
+
|
|
1253
|
+
// Add color variables if provided
|
|
1254
|
+
if (collection.docsColors) {
|
|
1255
|
+
const colors = collection.docsColors
|
|
1256
|
+
cssContent += `:root {\n`
|
|
1257
|
+
if (colors.primary) cssContent += ` --docs-primary: ${colors.primary};\n`
|
|
1258
|
+
if (colors.primaryLight) cssContent += ` --docs-primary-light: ${colors.primaryLight};\n`
|
|
1259
|
+
if (colors.primaryDark) cssContent += ` --docs-primary-dark: ${colors.primaryDark};\n`
|
|
1260
|
+
cssContent += `}\n\n`
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// Add custom CSS if provided
|
|
1264
|
+
if (collection.customCss) {
|
|
1265
|
+
cssContent += collection.customCss
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
styleEl.textContent = cssContent
|
|
1269
|
+
|
|
1270
|
+
return () => {
|
|
1271
|
+
// Cleanup on unmount
|
|
1272
|
+
const el = document.getElementById('docs-custom-css')
|
|
1273
|
+
if (el) el.remove()
|
|
1274
|
+
}
|
|
1275
|
+
}, [collection.customCss, collection.docsColors])
|
|
1276
|
+
|
|
1277
|
+
// Apply default theme from theme.json on initial load
|
|
1278
|
+
// Use a ref to track if we've already applied the default theme
|
|
1279
|
+
const hasAppliedDefaultTheme = useRef(false)
|
|
1280
|
+
useEffect(() => {
|
|
1281
|
+
if (collection?.defaultTheme && !hasAppliedDefaultTheme.current) {
|
|
1282
|
+
setTheme(collection.defaultTheme)
|
|
1283
|
+
hasAppliedDefaultTheme.current = true
|
|
1284
|
+
}
|
|
1285
|
+
}, [collection?.defaultTheme, setTheme])
|
|
1286
|
+
|
|
1287
|
+
return (
|
|
1288
|
+
<>
|
|
1289
|
+
{/* Notice */}
|
|
1290
|
+
{collection.notice && (
|
|
1291
|
+
<Notice config={collection.notice} storageKey="docs-notice" />
|
|
1292
|
+
)}
|
|
1293
|
+
|
|
1294
|
+
{/* Search Dialog */}
|
|
1295
|
+
<SearchDialog
|
|
1296
|
+
open={searchOpen}
|
|
1297
|
+
onOpenChange={setSearchOpen}
|
|
1298
|
+
items={searchItems}
|
|
1299
|
+
onSelect={handleSearchSelect}
|
|
1300
|
+
/>
|
|
1301
|
+
|
|
1302
|
+
{/* Header with tabs */}
|
|
1303
|
+
<DocsHeader
|
|
1304
|
+
docGroups={collection.docGroups}
|
|
1305
|
+
navigationTabs={collection.navigationTabs}
|
|
1306
|
+
activeTab={activeTab}
|
|
1307
|
+
onTabChange={onTabChange}
|
|
1308
|
+
hasEndpoints={hasEndpoints}
|
|
1309
|
+
docsName={collection.docsName}
|
|
1310
|
+
docsLogo={collection.docsLogo}
|
|
1311
|
+
onSearchClick={openSearch}
|
|
1312
|
+
docsHeader={collection.docsHeader}
|
|
1313
|
+
docsNavbar={collection.docsNavbar}
|
|
1314
|
+
/>
|
|
1315
|
+
|
|
1316
|
+
<div className="docs-layout flex h-[calc(100vh-6rem)] overflow-hidden relative z-0">
|
|
1317
|
+
{/* Left Sidebar - Hidden for changelog */}
|
|
1318
|
+
{!showChangelog && (
|
|
1319
|
+
<DocsSidebar
|
|
1320
|
+
collection={{
|
|
1321
|
+
...collection,
|
|
1322
|
+
// Show GraphQL operations when GraphQL tab is active, else show REST endpoints
|
|
1323
|
+
folders: showGraphQL && graphqlCollection
|
|
1324
|
+
? graphqlCollection.folders
|
|
1325
|
+
: (showEndpoints ? collection.folders : []),
|
|
1326
|
+
requests: showGraphQL && graphqlCollection
|
|
1327
|
+
? graphqlCollection.requests
|
|
1328
|
+
: (showEndpoints ? collection.requests : []),
|
|
1329
|
+
// Only show filtered doc groups
|
|
1330
|
+
docGroups: filteredDocGroups,
|
|
1331
|
+
}}
|
|
1332
|
+
selectedRequest={showGraphQL ? (selectedGraphQLOperation ? { id: selectedGraphQLOperation.id } as BrainfishRESTRequest : null) : selectedRequest}
|
|
1333
|
+
selectedDocSection={selectedDocSection}
|
|
1334
|
+
selectedDocPage={selectedDocPage}
|
|
1335
|
+
activeTab={activeTab}
|
|
1336
|
+
onSelectRequest={showGraphQL ? handleSelectGraphQLOperation : handleSelectRequest}
|
|
1337
|
+
onSelectDocumentation={handleSelectDocumentation}
|
|
1338
|
+
onSelectDocPage={handleSelectDocPage}
|
|
1339
|
+
apiVersions={collection.apiVersions}
|
|
1340
|
+
selectedApiVersion={selectedApiVersion}
|
|
1341
|
+
onApiVersionChange={handleApiVersionChange}
|
|
1342
|
+
isVersionLoading={isVersionLoading}
|
|
1343
|
+
/>
|
|
1344
|
+
)}
|
|
1345
|
+
|
|
1346
|
+
{/* Center - Toggles between API Client/Playground and Notes */}
|
|
1347
|
+
<div className="docs-main flex-1 flex flex-col overflow-hidden min-w-0">
|
|
1348
|
+
{/* Mode Toggle Header */}
|
|
1349
|
+
<div className="docs-main-header flex items-center justify-end px-3 sm:px-4 h-[41px] border-b border-border bg-muted/30">
|
|
1350
|
+
<ModeToggleTabs hasEndpoint={!!selectedRequest} />
|
|
1351
|
+
</div>
|
|
1352
|
+
|
|
1353
|
+
{/* Content Area */}
|
|
1354
|
+
{mode === 'docs' ? (
|
|
1355
|
+
<DocsNavigationProvider
|
|
1356
|
+
onNavigateToPage={handleSelectDocPage}
|
|
1357
|
+
onSwitchTab={onTabChange}
|
|
1358
|
+
activeTab={activeTab}
|
|
1359
|
+
>
|
|
1360
|
+
<div
|
|
1361
|
+
ref={contentRef}
|
|
1362
|
+
className={cn(
|
|
1363
|
+
"docs-content-area flex-1 bg-background min-w-0",
|
|
1364
|
+
(showChangelog || showGraphQL) ? "overflow-hidden flex flex-col" : "overflow-y-auto scroll-smooth"
|
|
1365
|
+
)}
|
|
1366
|
+
>
|
|
1367
|
+
{showGraphQL ? (
|
|
1368
|
+
<div className="flex-1 flex flex-col h-full">
|
|
1369
|
+
<GraphQLPlayground
|
|
1370
|
+
endpoint={activeGraphQLSchemas[0]?.endpoint || ''}
|
|
1371
|
+
defaultQuery={selectedGraphQLOperation?.query}
|
|
1372
|
+
operations={graphqlOperations}
|
|
1373
|
+
selectedOperationId={selectedGraphQLOperation?.id}
|
|
1374
|
+
hideExplorer={true}
|
|
1375
|
+
headers={{}}
|
|
1376
|
+
theme="dark"
|
|
1377
|
+
/>
|
|
1378
|
+
</div>
|
|
1379
|
+
) : showChangelog ? (
|
|
1380
|
+
<ChangelogPage
|
|
1381
|
+
releases={collection.changelogReleases || []}
|
|
1382
|
+
tabName={activeTabConfig?.tab}
|
|
1383
|
+
/>
|
|
1384
|
+
) : selectedRequest ? (
|
|
1385
|
+
<RequestDetails request={selectedRequest} />
|
|
1386
|
+
) : selectedDocPage ? (
|
|
1387
|
+
<DocPage slug={selectedDocPage} />
|
|
1388
|
+
) : (
|
|
1389
|
+
<Introduction collection={collection} />
|
|
1390
|
+
)}
|
|
1391
|
+
</div>
|
|
1392
|
+
</DocsNavigationProvider>
|
|
1393
|
+
) : mode === 'notes' ? (
|
|
1394
|
+
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
|
1395
|
+
<NotesMode apiSpecUrl={apiSpecUrl} apiName={collection.name} />
|
|
1396
|
+
</div>
|
|
1397
|
+
) : selectedRequest ? (
|
|
1398
|
+
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
|
1399
|
+
<ApiPlayground
|
|
1400
|
+
request={selectedRequest}
|
|
1401
|
+
onDebugRequest={handleDebugRequest}
|
|
1402
|
+
onExplainRequest={handleExplainRequest}
|
|
1403
|
+
/>
|
|
1404
|
+
</div>
|
|
1405
|
+
) : (
|
|
1406
|
+
<div className="flex-1 flex items-center justify-center bg-background">
|
|
1407
|
+
<div className="text-center text-muted-foreground">
|
|
1408
|
+
<TestTube className="h-12 w-12 mx-auto mb-3 opacity-40" />
|
|
1409
|
+
<p className="text-sm">Select an endpoint from the sidebar to test</p>
|
|
1410
|
+
</div>
|
|
1411
|
+
</div>
|
|
1412
|
+
)}
|
|
1413
|
+
</div>
|
|
1414
|
+
|
|
1415
|
+
{/* Right Sidebar - Agent Panel (drawer on mobile, always visible on desktop) */}
|
|
1416
|
+
<RightSidebar
|
|
1417
|
+
request={selectedRequest}
|
|
1418
|
+
collection={collection}
|
|
1419
|
+
apiSummary={collection.apiSummary}
|
|
1420
|
+
onNavigateToEndpoint={handleAgentNavigateWithModeSwitch}
|
|
1421
|
+
onPrefillParameters={handleAgentPrefill}
|
|
1422
|
+
debugContext={debugContext}
|
|
1423
|
+
onClearDebugContext={clearDebugContext}
|
|
1424
|
+
explainContext={explainContext}
|
|
1425
|
+
onClearExplainContext={clearExplainContext}
|
|
1426
|
+
onOpenGlobalAuth={() => setShowAuthModal(true)}
|
|
1427
|
+
onNavigateToAuthTab={() => {
|
|
1428
|
+
navigateAndHighlight('auth', { type: 'auth' }, true)
|
|
1429
|
+
}}
|
|
1430
|
+
onNavigateToParamsTab={() => {
|
|
1431
|
+
navigateAndHighlight('params', { type: 'param' }, true)
|
|
1432
|
+
}}
|
|
1433
|
+
onNavigateToBodyTab={() => {
|
|
1434
|
+
navigateAndHighlight('body', { type: 'body' }, true)
|
|
1435
|
+
}}
|
|
1436
|
+
onNavigateToHeadersTab={() => {
|
|
1437
|
+
navigateAndHighlight('headers', { type: 'header' }, true)
|
|
1438
|
+
}}
|
|
1439
|
+
onNavigateToDocSection={handleSelectDocumentation}
|
|
1440
|
+
onNavigateToDocPage={handleSelectDocPage}
|
|
1441
|
+
/>
|
|
1442
|
+
</div>
|
|
1443
|
+
|
|
1444
|
+
{/* Global Auth Modal */}
|
|
1445
|
+
<GlobalAuthModal
|
|
1446
|
+
open={showAuthModal}
|
|
1447
|
+
onClose={() => setShowAuthModal(false)}
|
|
1448
|
+
/>
|
|
1449
|
+
</>
|
|
1450
|
+
)
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// Wrapper component with providers
|
|
1454
|
+
export function Docs() {
|
|
1455
|
+
return (
|
|
1456
|
+
<AuthProvider>
|
|
1457
|
+
<ModeProvider>
|
|
1458
|
+
<PlaygroundProvider>
|
|
1459
|
+
<PlaygroundNavigationProvider>
|
|
1460
|
+
<DocsContent />
|
|
1461
|
+
</PlaygroundNavigationProvider>
|
|
1462
|
+
</PlaygroundProvider>
|
|
1463
|
+
</ModeProvider>
|
|
1464
|
+
</AuthProvider>
|
|
1465
|
+
)
|
|
1466
|
+
}
|