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