@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.
Files changed (268) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +415 -0
  3. package/bin/devdoc.js +13 -0
  4. package/dist/cli/commands/build.d.ts +5 -0
  5. package/dist/cli/commands/build.js +87 -0
  6. package/dist/cli/commands/check.d.ts +1 -0
  7. package/dist/cli/commands/check.js +143 -0
  8. package/dist/cli/commands/create.d.ts +24 -0
  9. package/dist/cli/commands/create.js +387 -0
  10. package/dist/cli/commands/deploy.d.ts +9 -0
  11. package/dist/cli/commands/deploy.js +433 -0
  12. package/dist/cli/commands/dev.d.ts +6 -0
  13. package/dist/cli/commands/dev.js +139 -0
  14. package/dist/cli/commands/init.d.ts +11 -0
  15. package/dist/cli/commands/init.js +238 -0
  16. package/dist/cli/commands/keys.d.ts +12 -0
  17. package/dist/cli/commands/keys.js +165 -0
  18. package/dist/cli/commands/start.d.ts +5 -0
  19. package/dist/cli/commands/start.js +56 -0
  20. package/dist/cli/commands/upload.d.ts +13 -0
  21. package/dist/cli/commands/upload.js +238 -0
  22. package/dist/cli/commands/whoami.d.ts +8 -0
  23. package/dist/cli/commands/whoami.js +91 -0
  24. package/dist/cli/index.d.ts +1 -0
  25. package/dist/cli/index.js +106 -0
  26. package/dist/config/index.d.ts +80 -0
  27. package/dist/config/index.js +133 -0
  28. package/dist/constants.d.ts +9 -0
  29. package/dist/constants.js +13 -0
  30. package/dist/index.d.ts +7 -0
  31. package/dist/index.js +12 -0
  32. package/dist/utils/logger.d.ts +16 -0
  33. package/dist/utils/logger.js +61 -0
  34. package/dist/utils/paths.d.ts +16 -0
  35. package/dist/utils/paths.js +50 -0
  36. package/package.json +51 -0
  37. package/renderer/app/api/assets/[...path]/route.ts +123 -0
  38. package/renderer/app/api/assets/route.ts +124 -0
  39. package/renderer/app/api/assets/upload/route.ts +177 -0
  40. package/renderer/app/api/auth-schemes/route.ts +77 -0
  41. package/renderer/app/api/chat/route.ts +858 -0
  42. package/renderer/app/api/codegen/route.ts +72 -0
  43. package/renderer/app/api/collections/route.ts +1016 -0
  44. package/renderer/app/api/debug/route.ts +53 -0
  45. package/renderer/app/api/deploy/route.ts +234 -0
  46. package/renderer/app/api/device/route.ts +42 -0
  47. package/renderer/app/api/docs/route.ts +187 -0
  48. package/renderer/app/api/keys/regenerate/route.ts +80 -0
  49. package/renderer/app/api/openapi-spec/route.ts +151 -0
  50. package/renderer/app/api/projects/[slug]/route.ts +153 -0
  51. package/renderer/app/api/projects/[slug]/stats/route.ts +96 -0
  52. package/renderer/app/api/projects/register/route.ts +152 -0
  53. package/renderer/app/api/proxy/route.ts +149 -0
  54. package/renderer/app/api/proxy-stream/route.ts +168 -0
  55. package/renderer/app/api/redirects/route.ts +47 -0
  56. package/renderer/app/api/schema/route.ts +65 -0
  57. package/renderer/app/api/subdomains/check/route.ts +172 -0
  58. package/renderer/app/api/suggestions/route.ts +144 -0
  59. package/renderer/app/favicon.ico +0 -0
  60. package/renderer/app/globals.css +1103 -0
  61. package/renderer/app/layout.tsx +47 -0
  62. package/renderer/app/llms-full.txt/route.ts +346 -0
  63. package/renderer/app/llms.txt/route.ts +279 -0
  64. package/renderer/app/page.tsx +14 -0
  65. package/renderer/app/robots.txt/route.ts +84 -0
  66. package/renderer/app/sitemap.xml/route.ts +199 -0
  67. package/renderer/components/docs/index.ts +12 -0
  68. package/renderer/components/docs/mdx/accordion.tsx +169 -0
  69. package/renderer/components/docs/mdx/badge.tsx +132 -0
  70. package/renderer/components/docs/mdx/callouts.tsx +154 -0
  71. package/renderer/components/docs/mdx/cards.tsx +213 -0
  72. package/renderer/components/docs/mdx/changelog.tsx +120 -0
  73. package/renderer/components/docs/mdx/code-block.tsx +186 -0
  74. package/renderer/components/docs/mdx/code-group.tsx +421 -0
  75. package/renderer/components/docs/mdx/file-embeds.tsx +105 -0
  76. package/renderer/components/docs/mdx/frame.tsx +112 -0
  77. package/renderer/components/docs/mdx/highlight.tsx +151 -0
  78. package/renderer/components/docs/mdx/iframe.tsx +134 -0
  79. package/renderer/components/docs/mdx/image.tsx +235 -0
  80. package/renderer/components/docs/mdx/index.ts +204 -0
  81. package/renderer/components/docs/mdx/mermaid.tsx +240 -0
  82. package/renderer/components/docs/mdx/param-field.tsx +200 -0
  83. package/renderer/components/docs/mdx/steps.tsx +113 -0
  84. package/renderer/components/docs/mdx/tabs.tsx +86 -0
  85. package/renderer/components/docs/mdx-renderer.tsx +100 -0
  86. package/renderer/components/docs/navigation/breadcrumbs.tsx +76 -0
  87. package/renderer/components/docs/navigation/index.ts +8 -0
  88. package/renderer/components/docs/navigation/page-nav.tsx +64 -0
  89. package/renderer/components/docs/navigation/sidebar.tsx +515 -0
  90. package/renderer/components/docs/navigation/toc.tsx +113 -0
  91. package/renderer/components/docs/notice.tsx +105 -0
  92. package/renderer/components/docs-header.tsx +274 -0
  93. package/renderer/components/docs-viewer/agent/agent-chat.tsx +2076 -0
  94. package/renderer/components/docs-viewer/agent/cards/debug-context-card.tsx +90 -0
  95. package/renderer/components/docs-viewer/agent/cards/endpoint-context-card.tsx +49 -0
  96. package/renderer/components/docs-viewer/agent/cards/index.tsx +50 -0
  97. package/renderer/components/docs-viewer/agent/cards/response-options-card.tsx +212 -0
  98. package/renderer/components/docs-viewer/agent/cards/types.ts +84 -0
  99. package/renderer/components/docs-viewer/agent/chat-message.tsx +17 -0
  100. package/renderer/components/docs-viewer/agent/index.tsx +6 -0
  101. package/renderer/components/docs-viewer/agent/messages/assistant-message.tsx +119 -0
  102. package/renderer/components/docs-viewer/agent/messages/chat-message.tsx +46 -0
  103. package/renderer/components/docs-viewer/agent/messages/index.ts +17 -0
  104. package/renderer/components/docs-viewer/agent/messages/tool-call-display.tsx +721 -0
  105. package/renderer/components/docs-viewer/agent/messages/types.ts +61 -0
  106. package/renderer/components/docs-viewer/agent/messages/typing-indicator.tsx +24 -0
  107. package/renderer/components/docs-viewer/agent/messages/user-message.tsx +51 -0
  108. package/renderer/components/docs-viewer/code-editor/index.tsx +2 -0
  109. package/renderer/components/docs-viewer/code-editor/notes-mode.tsx +1283 -0
  110. package/renderer/components/docs-viewer/content/changelog-page.tsx +331 -0
  111. package/renderer/components/docs-viewer/content/doc-page.tsx +285 -0
  112. package/renderer/components/docs-viewer/content/documentation-viewer.tsx +17 -0
  113. package/renderer/components/docs-viewer/content/index.tsx +29 -0
  114. package/renderer/components/docs-viewer/content/introduction.tsx +21 -0
  115. package/renderer/components/docs-viewer/content/request-details.tsx +330 -0
  116. package/renderer/components/docs-viewer/content/sections/auth.tsx +69 -0
  117. package/renderer/components/docs-viewer/content/sections/body.tsx +66 -0
  118. package/renderer/components/docs-viewer/content/sections/headers.tsx +43 -0
  119. package/renderer/components/docs-viewer/content/sections/overview.tsx +40 -0
  120. package/renderer/components/docs-viewer/content/sections/parameters.tsx +43 -0
  121. package/renderer/components/docs-viewer/content/sections/responses.tsx +87 -0
  122. package/renderer/components/docs-viewer/global-auth-modal.tsx +352 -0
  123. package/renderer/components/docs-viewer/index.tsx +1466 -0
  124. package/renderer/components/docs-viewer/playground/auth-editor.tsx +280 -0
  125. package/renderer/components/docs-viewer/playground/body-editor.tsx +221 -0
  126. package/renderer/components/docs-viewer/playground/code-editor.tsx +224 -0
  127. package/renderer/components/docs-viewer/playground/code-snippet.tsx +387 -0
  128. package/renderer/components/docs-viewer/playground/graphql-playground.tsx +745 -0
  129. package/renderer/components/docs-viewer/playground/index.tsx +671 -0
  130. package/renderer/components/docs-viewer/playground/key-value-editor.tsx +261 -0
  131. package/renderer/components/docs-viewer/playground/method-selector.tsx +60 -0
  132. package/renderer/components/docs-viewer/playground/request-builder.tsx +179 -0
  133. package/renderer/components/docs-viewer/playground/request-tabs.tsx +237 -0
  134. package/renderer/components/docs-viewer/playground/response-cards/idle-card.tsx +21 -0
  135. package/renderer/components/docs-viewer/playground/response-cards/index.tsx +93 -0
  136. package/renderer/components/docs-viewer/playground/response-cards/loading-card.tsx +16 -0
  137. package/renderer/components/docs-viewer/playground/response-cards/network-error-card.tsx +23 -0
  138. package/renderer/components/docs-viewer/playground/response-cards/response-body-card.tsx +268 -0
  139. package/renderer/components/docs-viewer/playground/response-cards/types.ts +82 -0
  140. package/renderer/components/docs-viewer/playground/response-viewer.tsx +43 -0
  141. package/renderer/components/docs-viewer/search/index.ts +2 -0
  142. package/renderer/components/docs-viewer/search/search-dialog.tsx +331 -0
  143. package/renderer/components/docs-viewer/search/use-search.ts +117 -0
  144. package/renderer/components/docs-viewer/shared/markdown-renderer.tsx +431 -0
  145. package/renderer/components/docs-viewer/shared/method-badge.tsx +41 -0
  146. package/renderer/components/docs-viewer/shared/schema-viewer.tsx +349 -0
  147. package/renderer/components/docs-viewer/sidebar/collection-tree.tsx +239 -0
  148. package/renderer/components/docs-viewer/sidebar/endpoint-options.tsx +316 -0
  149. package/renderer/components/docs-viewer/sidebar/index.tsx +343 -0
  150. package/renderer/components/docs-viewer/sidebar/right-sidebar.tsx +202 -0
  151. package/renderer/components/docs-viewer/sidebar/sidebar-group.tsx +118 -0
  152. package/renderer/components/docs-viewer/sidebar/sidebar-item.tsx +226 -0
  153. package/renderer/components/docs-viewer/sidebar/sidebar-section.tsx +52 -0
  154. package/renderer/components/theme-provider.tsx +11 -0
  155. package/renderer/components/theme-toggle.tsx +76 -0
  156. package/renderer/components/ui/badge.tsx +46 -0
  157. package/renderer/components/ui/button.tsx +59 -0
  158. package/renderer/components/ui/dialog.tsx +118 -0
  159. package/renderer/components/ui/dropdown-menu.tsx +257 -0
  160. package/renderer/components/ui/input.tsx +21 -0
  161. package/renderer/components/ui/label.tsx +24 -0
  162. package/renderer/components/ui/navigation-menu.tsx +168 -0
  163. package/renderer/components/ui/select.tsx +190 -0
  164. package/renderer/components/ui/spinner.tsx +114 -0
  165. package/renderer/components/ui/tabs.tsx +66 -0
  166. package/renderer/components/ui/tooltip.tsx +61 -0
  167. package/renderer/hooks/use-code-copy.ts +88 -0
  168. package/renderer/hooks/use-openapi-title.ts +44 -0
  169. package/renderer/lib/api-docs/agent/index.ts +6 -0
  170. package/renderer/lib/api-docs/agent/indexer.ts +323 -0
  171. package/renderer/lib/api-docs/agent/spec-summary.ts +335 -0
  172. package/renderer/lib/api-docs/agent/types.ts +116 -0
  173. package/renderer/lib/api-docs/auth/auth-context.tsx +225 -0
  174. package/renderer/lib/api-docs/auth/auth-storage.ts +87 -0
  175. package/renderer/lib/api-docs/auth/crypto.ts +89 -0
  176. package/renderer/lib/api-docs/auth/index.ts +4 -0
  177. package/renderer/lib/api-docs/code-editor/db.ts +164 -0
  178. package/renderer/lib/api-docs/code-editor/hooks.ts +266 -0
  179. package/renderer/lib/api-docs/code-editor/index.ts +6 -0
  180. package/renderer/lib/api-docs/code-editor/mode-context.tsx +207 -0
  181. package/renderer/lib/api-docs/code-editor/types.ts +105 -0
  182. package/renderer/lib/api-docs/codegen/definitions.ts +297 -0
  183. package/renderer/lib/api-docs/codegen/har.ts +251 -0
  184. package/renderer/lib/api-docs/codegen/index.ts +159 -0
  185. package/renderer/lib/api-docs/factories.ts +151 -0
  186. package/renderer/lib/api-docs/index.ts +17 -0
  187. package/renderer/lib/api-docs/mobile-context.tsx +112 -0
  188. package/renderer/lib/api-docs/navigation-context.tsx +88 -0
  189. package/renderer/lib/api-docs/parsers/graphql/README.md +129 -0
  190. package/renderer/lib/api-docs/parsers/graphql/index.ts +91 -0
  191. package/renderer/lib/api-docs/parsers/graphql/parser.ts +491 -0
  192. package/renderer/lib/api-docs/parsers/graphql/transformer.ts +246 -0
  193. package/renderer/lib/api-docs/parsers/graphql/types.ts +283 -0
  194. package/renderer/lib/api-docs/parsers/openapi/README.md +32 -0
  195. package/renderer/lib/api-docs/parsers/openapi/dereferencer.ts +60 -0
  196. package/renderer/lib/api-docs/parsers/openapi/extractors/auth.ts +574 -0
  197. package/renderer/lib/api-docs/parsers/openapi/extractors/body.ts +403 -0
  198. package/renderer/lib/api-docs/parsers/openapi/extractors/index.ts +232 -0
  199. package/renderer/lib/api-docs/parsers/openapi/index.ts +171 -0
  200. package/renderer/lib/api-docs/parsers/openapi/transformer.ts +277 -0
  201. package/renderer/lib/api-docs/parsers/openapi/validator.ts +31 -0
  202. package/renderer/lib/api-docs/playground/context.tsx +107 -0
  203. package/renderer/lib/api-docs/playground/navigation-context.tsx +124 -0
  204. package/renderer/lib/api-docs/playground/request-builder.ts +223 -0
  205. package/renderer/lib/api-docs/playground/request-runner.ts +282 -0
  206. package/renderer/lib/api-docs/playground/types.ts +35 -0
  207. package/renderer/lib/api-docs/types.ts +269 -0
  208. package/renderer/lib/api-docs/utils.ts +311 -0
  209. package/renderer/lib/cache.ts +193 -0
  210. package/renderer/lib/docs/config/index.ts +29 -0
  211. package/renderer/lib/docs/config/loader.ts +142 -0
  212. package/renderer/lib/docs/config/schema.ts +298 -0
  213. package/renderer/lib/docs/index.ts +12 -0
  214. package/renderer/lib/docs/mdx/compiler.ts +176 -0
  215. package/renderer/lib/docs/mdx/frontmatter.ts +80 -0
  216. package/renderer/lib/docs/mdx/index.ts +26 -0
  217. package/renderer/lib/docs/navigation/generator.ts +348 -0
  218. package/renderer/lib/docs/navigation/index.ts +12 -0
  219. package/renderer/lib/docs/navigation/types.ts +123 -0
  220. package/renderer/lib/docs-navigation-context.tsx +80 -0
  221. package/renderer/lib/multi-tenant/context.ts +105 -0
  222. package/renderer/lib/storage/blob.ts +845 -0
  223. package/renderer/lib/utils.ts +6 -0
  224. package/renderer/next.config.ts +76 -0
  225. package/renderer/package.json +66 -0
  226. package/renderer/postcss.config.mjs +5 -0
  227. package/renderer/public/assets/images/screenshot.png +0 -0
  228. package/renderer/public/assets/logo/dark.svg +9 -0
  229. package/renderer/public/assets/logo/light.svg +9 -0
  230. package/renderer/public/assets/logo.svg +9 -0
  231. package/renderer/public/file.svg +1 -0
  232. package/renderer/public/globe.svg +1 -0
  233. package/renderer/public/icon.png +0 -0
  234. package/renderer/public/logo.svg +9 -0
  235. package/renderer/public/window.svg +1 -0
  236. package/renderer/tsconfig.json +28 -0
  237. package/templates/basic/README.md +139 -0
  238. package/templates/basic/assets/favicon.svg +4 -0
  239. package/templates/basic/assets/logo.svg +9 -0
  240. package/templates/basic/docs.json +47 -0
  241. package/templates/basic/guides/configuration.mdx +149 -0
  242. package/templates/basic/guides/overview.mdx +96 -0
  243. package/templates/basic/index.mdx +39 -0
  244. package/templates/basic/package.json +14 -0
  245. package/templates/basic/quickstart.mdx +92 -0
  246. package/templates/basic/vercel.json +6 -0
  247. package/templates/graphql/README.md +139 -0
  248. package/templates/graphql/api-reference/schema.graphql +305 -0
  249. package/templates/graphql/assets/favicon.svg +4 -0
  250. package/templates/graphql/assets/logo.svg +9 -0
  251. package/templates/graphql/docs.json +54 -0
  252. package/templates/graphql/guides/configuration.mdx +149 -0
  253. package/templates/graphql/guides/overview.mdx +96 -0
  254. package/templates/graphql/index.mdx +39 -0
  255. package/templates/graphql/package.json +14 -0
  256. package/templates/graphql/quickstart.mdx +92 -0
  257. package/templates/graphql/vercel.json +6 -0
  258. package/templates/openapi/README.md +139 -0
  259. package/templates/openapi/api-reference/openapi.json +419 -0
  260. package/templates/openapi/assets/favicon.svg +4 -0
  261. package/templates/openapi/assets/logo.svg +9 -0
  262. package/templates/openapi/docs.json +61 -0
  263. package/templates/openapi/guides/configuration.mdx +149 -0
  264. package/templates/openapi/guides/overview.mdx +96 -0
  265. package/templates/openapi/index.mdx +39 -0
  266. package/templates/openapi/package.json +14 -0
  267. package/templates/openapi/quickstart.mdx +92 -0
  268. 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
+ }