@brainfish-ai/devdoc 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +33 -0
- package/README.md +415 -0
- package/bin/devdoc.js +13 -0
- package/dist/cli/commands/build.d.ts +5 -0
- package/dist/cli/commands/build.js +87 -0
- package/dist/cli/commands/check.d.ts +1 -0
- package/dist/cli/commands/check.js +143 -0
- package/dist/cli/commands/create.d.ts +24 -0
- package/dist/cli/commands/create.js +387 -0
- package/dist/cli/commands/deploy.d.ts +9 -0
- package/dist/cli/commands/deploy.js +433 -0
- package/dist/cli/commands/dev.d.ts +6 -0
- package/dist/cli/commands/dev.js +139 -0
- package/dist/cli/commands/init.d.ts +11 -0
- package/dist/cli/commands/init.js +238 -0
- package/dist/cli/commands/keys.d.ts +12 -0
- package/dist/cli/commands/keys.js +165 -0
- package/dist/cli/commands/start.d.ts +5 -0
- package/dist/cli/commands/start.js +56 -0
- package/dist/cli/commands/upload.d.ts +13 -0
- package/dist/cli/commands/upload.js +238 -0
- package/dist/cli/commands/whoami.d.ts +8 -0
- package/dist/cli/commands/whoami.js +91 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +106 -0
- package/dist/config/index.d.ts +80 -0
- package/dist/config/index.js +133 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +13 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +61 -0
- package/dist/utils/paths.d.ts +16 -0
- package/dist/utils/paths.js +50 -0
- package/package.json +51 -0
- package/renderer/app/api/assets/[...path]/route.ts +123 -0
- package/renderer/app/api/assets/route.ts +124 -0
- package/renderer/app/api/assets/upload/route.ts +177 -0
- package/renderer/app/api/auth-schemes/route.ts +77 -0
- package/renderer/app/api/chat/route.ts +858 -0
- package/renderer/app/api/codegen/route.ts +72 -0
- package/renderer/app/api/collections/route.ts +1016 -0
- package/renderer/app/api/debug/route.ts +53 -0
- package/renderer/app/api/deploy/route.ts +234 -0
- package/renderer/app/api/device/route.ts +42 -0
- package/renderer/app/api/docs/route.ts +187 -0
- package/renderer/app/api/keys/regenerate/route.ts +80 -0
- package/renderer/app/api/openapi-spec/route.ts +151 -0
- package/renderer/app/api/projects/[slug]/route.ts +153 -0
- package/renderer/app/api/projects/[slug]/stats/route.ts +96 -0
- package/renderer/app/api/projects/register/route.ts +152 -0
- package/renderer/app/api/proxy/route.ts +149 -0
- package/renderer/app/api/proxy-stream/route.ts +168 -0
- package/renderer/app/api/redirects/route.ts +47 -0
- package/renderer/app/api/schema/route.ts +65 -0
- package/renderer/app/api/subdomains/check/route.ts +172 -0
- package/renderer/app/api/suggestions/route.ts +144 -0
- package/renderer/app/favicon.ico +0 -0
- package/renderer/app/globals.css +1103 -0
- package/renderer/app/layout.tsx +47 -0
- package/renderer/app/llms-full.txt/route.ts +346 -0
- package/renderer/app/llms.txt/route.ts +279 -0
- package/renderer/app/page.tsx +14 -0
- package/renderer/app/robots.txt/route.ts +84 -0
- package/renderer/app/sitemap.xml/route.ts +199 -0
- package/renderer/components/docs/index.ts +12 -0
- package/renderer/components/docs/mdx/accordion.tsx +169 -0
- package/renderer/components/docs/mdx/badge.tsx +132 -0
- package/renderer/components/docs/mdx/callouts.tsx +154 -0
- package/renderer/components/docs/mdx/cards.tsx +213 -0
- package/renderer/components/docs/mdx/changelog.tsx +120 -0
- package/renderer/components/docs/mdx/code-block.tsx +186 -0
- package/renderer/components/docs/mdx/code-group.tsx +421 -0
- package/renderer/components/docs/mdx/file-embeds.tsx +105 -0
- package/renderer/components/docs/mdx/frame.tsx +112 -0
- package/renderer/components/docs/mdx/highlight.tsx +151 -0
- package/renderer/components/docs/mdx/iframe.tsx +134 -0
- package/renderer/components/docs/mdx/image.tsx +235 -0
- package/renderer/components/docs/mdx/index.ts +204 -0
- package/renderer/components/docs/mdx/mermaid.tsx +240 -0
- package/renderer/components/docs/mdx/param-field.tsx +200 -0
- package/renderer/components/docs/mdx/steps.tsx +113 -0
- package/renderer/components/docs/mdx/tabs.tsx +86 -0
- package/renderer/components/docs/mdx-renderer.tsx +100 -0
- package/renderer/components/docs/navigation/breadcrumbs.tsx +76 -0
- package/renderer/components/docs/navigation/index.ts +8 -0
- package/renderer/components/docs/navigation/page-nav.tsx +64 -0
- package/renderer/components/docs/navigation/sidebar.tsx +515 -0
- package/renderer/components/docs/navigation/toc.tsx +113 -0
- package/renderer/components/docs/notice.tsx +105 -0
- package/renderer/components/docs-header.tsx +274 -0
- package/renderer/components/docs-viewer/agent/agent-chat.tsx +2076 -0
- package/renderer/components/docs-viewer/agent/cards/debug-context-card.tsx +90 -0
- package/renderer/components/docs-viewer/agent/cards/endpoint-context-card.tsx +49 -0
- package/renderer/components/docs-viewer/agent/cards/index.tsx +50 -0
- package/renderer/components/docs-viewer/agent/cards/response-options-card.tsx +212 -0
- package/renderer/components/docs-viewer/agent/cards/types.ts +84 -0
- package/renderer/components/docs-viewer/agent/chat-message.tsx +17 -0
- package/renderer/components/docs-viewer/agent/index.tsx +6 -0
- package/renderer/components/docs-viewer/agent/messages/assistant-message.tsx +119 -0
- package/renderer/components/docs-viewer/agent/messages/chat-message.tsx +46 -0
- package/renderer/components/docs-viewer/agent/messages/index.ts +17 -0
- package/renderer/components/docs-viewer/agent/messages/tool-call-display.tsx +721 -0
- package/renderer/components/docs-viewer/agent/messages/types.ts +61 -0
- package/renderer/components/docs-viewer/agent/messages/typing-indicator.tsx +24 -0
- package/renderer/components/docs-viewer/agent/messages/user-message.tsx +51 -0
- package/renderer/components/docs-viewer/code-editor/index.tsx +2 -0
- package/renderer/components/docs-viewer/code-editor/notes-mode.tsx +1283 -0
- package/renderer/components/docs-viewer/content/changelog-page.tsx +331 -0
- package/renderer/components/docs-viewer/content/doc-page.tsx +285 -0
- package/renderer/components/docs-viewer/content/documentation-viewer.tsx +17 -0
- package/renderer/components/docs-viewer/content/index.tsx +29 -0
- package/renderer/components/docs-viewer/content/introduction.tsx +21 -0
- package/renderer/components/docs-viewer/content/request-details.tsx +330 -0
- package/renderer/components/docs-viewer/content/sections/auth.tsx +69 -0
- package/renderer/components/docs-viewer/content/sections/body.tsx +66 -0
- package/renderer/components/docs-viewer/content/sections/headers.tsx +43 -0
- package/renderer/components/docs-viewer/content/sections/overview.tsx +40 -0
- package/renderer/components/docs-viewer/content/sections/parameters.tsx +43 -0
- package/renderer/components/docs-viewer/content/sections/responses.tsx +87 -0
- package/renderer/components/docs-viewer/global-auth-modal.tsx +352 -0
- package/renderer/components/docs-viewer/index.tsx +1466 -0
- package/renderer/components/docs-viewer/playground/auth-editor.tsx +280 -0
- package/renderer/components/docs-viewer/playground/body-editor.tsx +221 -0
- package/renderer/components/docs-viewer/playground/code-editor.tsx +224 -0
- package/renderer/components/docs-viewer/playground/code-snippet.tsx +387 -0
- package/renderer/components/docs-viewer/playground/graphql-playground.tsx +745 -0
- package/renderer/components/docs-viewer/playground/index.tsx +671 -0
- package/renderer/components/docs-viewer/playground/key-value-editor.tsx +261 -0
- package/renderer/components/docs-viewer/playground/method-selector.tsx +60 -0
- package/renderer/components/docs-viewer/playground/request-builder.tsx +179 -0
- package/renderer/components/docs-viewer/playground/request-tabs.tsx +237 -0
- package/renderer/components/docs-viewer/playground/response-cards/idle-card.tsx +21 -0
- package/renderer/components/docs-viewer/playground/response-cards/index.tsx +93 -0
- package/renderer/components/docs-viewer/playground/response-cards/loading-card.tsx +16 -0
- package/renderer/components/docs-viewer/playground/response-cards/network-error-card.tsx +23 -0
- package/renderer/components/docs-viewer/playground/response-cards/response-body-card.tsx +268 -0
- package/renderer/components/docs-viewer/playground/response-cards/types.ts +82 -0
- package/renderer/components/docs-viewer/playground/response-viewer.tsx +43 -0
- package/renderer/components/docs-viewer/search/index.ts +2 -0
- package/renderer/components/docs-viewer/search/search-dialog.tsx +331 -0
- package/renderer/components/docs-viewer/search/use-search.ts +117 -0
- package/renderer/components/docs-viewer/shared/markdown-renderer.tsx +431 -0
- package/renderer/components/docs-viewer/shared/method-badge.tsx +41 -0
- package/renderer/components/docs-viewer/shared/schema-viewer.tsx +349 -0
- package/renderer/components/docs-viewer/sidebar/collection-tree.tsx +239 -0
- package/renderer/components/docs-viewer/sidebar/endpoint-options.tsx +316 -0
- package/renderer/components/docs-viewer/sidebar/index.tsx +343 -0
- package/renderer/components/docs-viewer/sidebar/right-sidebar.tsx +202 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-group.tsx +118 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-item.tsx +226 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-section.tsx +52 -0
- package/renderer/components/theme-provider.tsx +11 -0
- package/renderer/components/theme-toggle.tsx +76 -0
- package/renderer/components/ui/badge.tsx +46 -0
- package/renderer/components/ui/button.tsx +59 -0
- package/renderer/components/ui/dialog.tsx +118 -0
- package/renderer/components/ui/dropdown-menu.tsx +257 -0
- package/renderer/components/ui/input.tsx +21 -0
- package/renderer/components/ui/label.tsx +24 -0
- package/renderer/components/ui/navigation-menu.tsx +168 -0
- package/renderer/components/ui/select.tsx +190 -0
- package/renderer/components/ui/spinner.tsx +114 -0
- package/renderer/components/ui/tabs.tsx +66 -0
- package/renderer/components/ui/tooltip.tsx +61 -0
- package/renderer/hooks/use-code-copy.ts +88 -0
- package/renderer/hooks/use-openapi-title.ts +44 -0
- package/renderer/lib/api-docs/agent/index.ts +6 -0
- package/renderer/lib/api-docs/agent/indexer.ts +323 -0
- package/renderer/lib/api-docs/agent/spec-summary.ts +335 -0
- package/renderer/lib/api-docs/agent/types.ts +116 -0
- package/renderer/lib/api-docs/auth/auth-context.tsx +225 -0
- package/renderer/lib/api-docs/auth/auth-storage.ts +87 -0
- package/renderer/lib/api-docs/auth/crypto.ts +89 -0
- package/renderer/lib/api-docs/auth/index.ts +4 -0
- package/renderer/lib/api-docs/code-editor/db.ts +164 -0
- package/renderer/lib/api-docs/code-editor/hooks.ts +266 -0
- package/renderer/lib/api-docs/code-editor/index.ts +6 -0
- package/renderer/lib/api-docs/code-editor/mode-context.tsx +207 -0
- package/renderer/lib/api-docs/code-editor/types.ts +105 -0
- package/renderer/lib/api-docs/codegen/definitions.ts +297 -0
- package/renderer/lib/api-docs/codegen/har.ts +251 -0
- package/renderer/lib/api-docs/codegen/index.ts +159 -0
- package/renderer/lib/api-docs/factories.ts +151 -0
- package/renderer/lib/api-docs/index.ts +17 -0
- package/renderer/lib/api-docs/mobile-context.tsx +112 -0
- package/renderer/lib/api-docs/navigation-context.tsx +88 -0
- package/renderer/lib/api-docs/parsers/graphql/README.md +129 -0
- package/renderer/lib/api-docs/parsers/graphql/index.ts +91 -0
- package/renderer/lib/api-docs/parsers/graphql/parser.ts +491 -0
- package/renderer/lib/api-docs/parsers/graphql/transformer.ts +246 -0
- package/renderer/lib/api-docs/parsers/graphql/types.ts +283 -0
- package/renderer/lib/api-docs/parsers/openapi/README.md +32 -0
- package/renderer/lib/api-docs/parsers/openapi/dereferencer.ts +60 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/auth.ts +574 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/body.ts +403 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/index.ts +232 -0
- package/renderer/lib/api-docs/parsers/openapi/index.ts +171 -0
- package/renderer/lib/api-docs/parsers/openapi/transformer.ts +277 -0
- package/renderer/lib/api-docs/parsers/openapi/validator.ts +31 -0
- package/renderer/lib/api-docs/playground/context.tsx +107 -0
- package/renderer/lib/api-docs/playground/navigation-context.tsx +124 -0
- package/renderer/lib/api-docs/playground/request-builder.ts +223 -0
- package/renderer/lib/api-docs/playground/request-runner.ts +282 -0
- package/renderer/lib/api-docs/playground/types.ts +35 -0
- package/renderer/lib/api-docs/types.ts +269 -0
- package/renderer/lib/api-docs/utils.ts +311 -0
- package/renderer/lib/cache.ts +193 -0
- package/renderer/lib/docs/config/index.ts +29 -0
- package/renderer/lib/docs/config/loader.ts +142 -0
- package/renderer/lib/docs/config/schema.ts +298 -0
- package/renderer/lib/docs/index.ts +12 -0
- package/renderer/lib/docs/mdx/compiler.ts +176 -0
- package/renderer/lib/docs/mdx/frontmatter.ts +80 -0
- package/renderer/lib/docs/mdx/index.ts +26 -0
- package/renderer/lib/docs/navigation/generator.ts +348 -0
- package/renderer/lib/docs/navigation/index.ts +12 -0
- package/renderer/lib/docs/navigation/types.ts +123 -0
- package/renderer/lib/docs-navigation-context.tsx +80 -0
- package/renderer/lib/multi-tenant/context.ts +105 -0
- package/renderer/lib/storage/blob.ts +845 -0
- package/renderer/lib/utils.ts +6 -0
- package/renderer/next.config.ts +76 -0
- package/renderer/package.json +66 -0
- package/renderer/postcss.config.mjs +5 -0
- package/renderer/public/assets/images/screenshot.png +0 -0
- package/renderer/public/assets/logo/dark.svg +9 -0
- package/renderer/public/assets/logo/light.svg +9 -0
- package/renderer/public/assets/logo.svg +9 -0
- package/renderer/public/file.svg +1 -0
- package/renderer/public/globe.svg +1 -0
- package/renderer/public/icon.png +0 -0
- package/renderer/public/logo.svg +9 -0
- package/renderer/public/window.svg +1 -0
- package/renderer/tsconfig.json +28 -0
- package/templates/basic/README.md +139 -0
- package/templates/basic/assets/favicon.svg +4 -0
- package/templates/basic/assets/logo.svg +9 -0
- package/templates/basic/docs.json +47 -0
- package/templates/basic/guides/configuration.mdx +149 -0
- package/templates/basic/guides/overview.mdx +96 -0
- package/templates/basic/index.mdx +39 -0
- package/templates/basic/package.json +14 -0
- package/templates/basic/quickstart.mdx +92 -0
- package/templates/basic/vercel.json +6 -0
- package/templates/graphql/README.md +139 -0
- package/templates/graphql/api-reference/schema.graphql +305 -0
- package/templates/graphql/assets/favicon.svg +4 -0
- package/templates/graphql/assets/logo.svg +9 -0
- package/templates/graphql/docs.json +54 -0
- package/templates/graphql/guides/configuration.mdx +149 -0
- package/templates/graphql/guides/overview.mdx +96 -0
- package/templates/graphql/index.mdx +39 -0
- package/templates/graphql/package.json +14 -0
- package/templates/graphql/quickstart.mdx +92 -0
- package/templates/graphql/vercel.json +6 -0
- package/templates/openapi/README.md +139 -0
- package/templates/openapi/api-reference/openapi.json +419 -0
- package/templates/openapi/assets/favicon.svg +4 -0
- package/templates/openapi/assets/logo.svg +9 -0
- package/templates/openapi/docs.json +61 -0
- package/templates/openapi/guides/configuration.mdx +149 -0
- package/templates/openapi/guides/overview.mdx +96 -0
- package/templates/openapi/index.mdx +39 -0
- package/templates/openapi/package.json +14 -0
- package/templates/openapi/quickstart.mdx +92 -0
- package/templates/openapi/vercel.json +6 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Response card type identifiers
|
|
2
|
+
export type ResponseCardType =
|
|
3
|
+
| 'idle' // No request sent yet
|
|
4
|
+
| 'loading' // Request in progress
|
|
5
|
+
| 'success' // 2xx response
|
|
6
|
+
| 'error' // 4xx/5xx response or network error
|
|
7
|
+
| 'streaming' // SSE/streaming response
|
|
8
|
+
|
|
9
|
+
// Response metadata
|
|
10
|
+
export interface ResponseMeta {
|
|
11
|
+
status: number
|
|
12
|
+
statusText: string
|
|
13
|
+
responseTime: number
|
|
14
|
+
size: number
|
|
15
|
+
isStreaming?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Base interface for response card props
|
|
19
|
+
export interface BaseResponseCardProps {
|
|
20
|
+
type: ResponseCardType
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Idle state - no request sent
|
|
24
|
+
export interface IdleCardProps extends BaseResponseCardProps {
|
|
25
|
+
type: 'idle'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Loading state
|
|
29
|
+
export interface LoadingCardProps extends BaseResponseCardProps {
|
|
30
|
+
type: 'loading'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Success response (2xx)
|
|
34
|
+
export interface SuccessCardProps extends BaseResponseCardProps {
|
|
35
|
+
type: 'success'
|
|
36
|
+
meta: ResponseMeta
|
|
37
|
+
body: string
|
|
38
|
+
headers: Record<string, string>
|
|
39
|
+
contentType: string | null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Error response (4xx/5xx or network error)
|
|
43
|
+
export interface ErrorCardProps extends BaseResponseCardProps {
|
|
44
|
+
type: 'error'
|
|
45
|
+
meta?: ResponseMeta
|
|
46
|
+
body?: string
|
|
47
|
+
headers?: Record<string, string>
|
|
48
|
+
contentType?: string | null
|
|
49
|
+
errorMessage?: string
|
|
50
|
+
// Debug callback
|
|
51
|
+
onDebug?: () => void
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Streaming response
|
|
55
|
+
export interface StreamingCardProps extends BaseResponseCardProps {
|
|
56
|
+
type: 'streaming'
|
|
57
|
+
meta: ResponseMeta
|
|
58
|
+
body: string
|
|
59
|
+
headers: Record<string, string>
|
|
60
|
+
contentType: string | null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Union type for all response card props
|
|
64
|
+
export type ResponseCardProps =
|
|
65
|
+
| IdleCardProps
|
|
66
|
+
| LoadingCardProps
|
|
67
|
+
| SuccessCardProps
|
|
68
|
+
| ErrorCardProps
|
|
69
|
+
| StreamingCardProps
|
|
70
|
+
|
|
71
|
+
// Helper to determine card type from status
|
|
72
|
+
export function getResponseCardType(
|
|
73
|
+
status: number | null,
|
|
74
|
+
isStreaming: boolean = false,
|
|
75
|
+
hasError: boolean = false
|
|
76
|
+
): ResponseCardType {
|
|
77
|
+
if (isStreaming) return 'streaming'
|
|
78
|
+
if (hasError && status === null) return 'error'
|
|
79
|
+
if (status === null) return 'idle'
|
|
80
|
+
if (status >= 200 && status < 300) return 'success'
|
|
81
|
+
return 'error'
|
|
82
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { ResponseCardRenderer } from './response-cards'
|
|
4
|
+
import type { PlaygroundResponseState } from '@/lib/api-docs/playground/types'
|
|
5
|
+
|
|
6
|
+
// Re-export DebugContext type for consumers
|
|
7
|
+
export interface DebugContext {
|
|
8
|
+
// Response info
|
|
9
|
+
status: number
|
|
10
|
+
statusText: string
|
|
11
|
+
responseBody: string
|
|
12
|
+
errorMessage?: string
|
|
13
|
+
// Endpoint info (populated by parent)
|
|
14
|
+
endpointName?: string
|
|
15
|
+
endpointMethod?: string
|
|
16
|
+
endpointPath?: string
|
|
17
|
+
requestBody?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ResponseViewerProps {
|
|
21
|
+
responseState: PlaygroundResponseState
|
|
22
|
+
onDebugRequest?: (context: DebugContext) => void
|
|
23
|
+
onExplainRequest?: (context: DebugContext) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* ResponseViewer - displays API response with support for different states
|
|
28
|
+
*
|
|
29
|
+
* Uses modular card components from response-cards/ folder:
|
|
30
|
+
* - IdleCard: No request sent yet
|
|
31
|
+
* - LoadingCard: Request in progress
|
|
32
|
+
* - NetworkErrorCard: Network/connection error
|
|
33
|
+
* - ResponseBodyCard: Success or error with response body
|
|
34
|
+
*/
|
|
35
|
+
export function ResponseViewer({ responseState, onDebugRequest, onExplainRequest }: ResponseViewerProps) {
|
|
36
|
+
return (
|
|
37
|
+
<ResponseCardRenderer
|
|
38
|
+
responseState={responseState}
|
|
39
|
+
onDebugRequest={onDebugRequest}
|
|
40
|
+
onExplainRequest={onExplainRequest}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
|
4
|
+
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
|
|
5
|
+
import * as VisuallyHidden from '@radix-ui/react-visually-hidden'
|
|
6
|
+
import { MagnifyingGlass, File, Code, ArrowRight, BookOpen, Lightning } from '@phosphor-icons/react'
|
|
7
|
+
import { cn } from '@/lib/utils'
|
|
8
|
+
|
|
9
|
+
interface SearchItem {
|
|
10
|
+
id: string
|
|
11
|
+
title: string
|
|
12
|
+
description?: string
|
|
13
|
+
type: 'endpoint' | 'doc' | 'folder'
|
|
14
|
+
method?: string
|
|
15
|
+
path?: string
|
|
16
|
+
href: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SearchDialogProps {
|
|
20
|
+
open: boolean
|
|
21
|
+
onOpenChange: (open: boolean) => void
|
|
22
|
+
items: SearchItem[]
|
|
23
|
+
onSelect: (item: SearchItem) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const METHOD_COLORS: Record<string, string> = {
|
|
27
|
+
GET: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
|
|
28
|
+
POST: 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
|
|
29
|
+
PUT: 'bg-amber-500/10 text-amber-600 dark:text-amber-400',
|
|
30
|
+
PATCH: 'bg-orange-500/10 text-orange-600 dark:text-orange-400',
|
|
31
|
+
DELETE: 'bg-red-500/10 text-red-600 dark:text-red-400',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface GroupedResults {
|
|
35
|
+
docs: SearchItem[]
|
|
36
|
+
endpoints: SearchItem[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function SearchDialog({ open, onOpenChange, items, onSelect }: SearchDialogProps) {
|
|
40
|
+
const [query, setQuery] = useState('')
|
|
41
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
42
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
43
|
+
const listRef = useRef<HTMLDivElement>(null)
|
|
44
|
+
|
|
45
|
+
// Filter and group items based on query with fuzzy/BM25-like scoring
|
|
46
|
+
const { groupedResults } = useMemo(() => {
|
|
47
|
+
let filtered: SearchItem[]
|
|
48
|
+
|
|
49
|
+
if (!query.trim()) {
|
|
50
|
+
// Show a mix of docs and endpoints when no query
|
|
51
|
+
const docs = items.filter(i => i.type === 'doc').slice(0, 15)
|
|
52
|
+
const endpoints = items.filter(i => i.type === 'endpoint').slice(0, 15)
|
|
53
|
+
filtered = [...docs, ...endpoints]
|
|
54
|
+
} else {
|
|
55
|
+
// Tokenize query
|
|
56
|
+
const queryTokens = query.toLowerCase().split(/\s+/).filter(t => t.length > 1)
|
|
57
|
+
const queryLower = query.toLowerCase()
|
|
58
|
+
|
|
59
|
+
// Score each item
|
|
60
|
+
const scored = items.map(item => {
|
|
61
|
+
let score = 0
|
|
62
|
+
const titleLower = item.title.toLowerCase()
|
|
63
|
+
const descLower = (item.description || '').toLowerCase()
|
|
64
|
+
const pathLower = (item.path || '').toLowerCase()
|
|
65
|
+
const hrefLower = item.href.toLowerCase()
|
|
66
|
+
|
|
67
|
+
// Exact phrase match (highest priority)
|
|
68
|
+
if (titleLower.includes(queryLower)) score += 100
|
|
69
|
+
if (pathLower.includes(queryLower)) score += 80
|
|
70
|
+
if (descLower.includes(queryLower)) score += 60
|
|
71
|
+
if (hrefLower.includes(queryLower)) score += 40
|
|
72
|
+
|
|
73
|
+
// Token matching
|
|
74
|
+
for (const token of queryTokens) {
|
|
75
|
+
// Title matches
|
|
76
|
+
if (titleLower === token) score += 50
|
|
77
|
+
else if (titleLower.startsWith(token)) score += 30
|
|
78
|
+
else if (titleLower.includes(token)) score += 20
|
|
79
|
+
|
|
80
|
+
// Path matches (for endpoints and docs)
|
|
81
|
+
if (pathLower.includes(token)) score += 25
|
|
82
|
+
const pathParts = pathLower.split(/[\/\s-]/)
|
|
83
|
+
if (pathParts.some(part => part === token)) score += 35
|
|
84
|
+
if (pathParts.some(part => part.startsWith(token))) score += 20
|
|
85
|
+
|
|
86
|
+
// Description matches
|
|
87
|
+
if (descLower.includes(token)) score += 15
|
|
88
|
+
|
|
89
|
+
// Href/slug matches
|
|
90
|
+
const hrefParts = hrefLower.split(/[\/\s-#]/)
|
|
91
|
+
if (hrefParts.some(part => part === token)) score += 30
|
|
92
|
+
if (hrefParts.some(part => part.startsWith(token))) score += 15
|
|
93
|
+
|
|
94
|
+
// Method matching for endpoints
|
|
95
|
+
if (item.method && item.method.toLowerCase() === token) score += 25
|
|
96
|
+
|
|
97
|
+
// Fuzzy matching
|
|
98
|
+
const allWords = [titleLower, ...pathParts, ...hrefParts].filter(Boolean)
|
|
99
|
+
for (const word of allWords) {
|
|
100
|
+
if (word.length > 2 && token.length > 2) {
|
|
101
|
+
const minLen = Math.min(word.length, token.length)
|
|
102
|
+
let matches = 0
|
|
103
|
+
for (let i = 0; i < minLen; i++) {
|
|
104
|
+
if (word[i] === token[i]) matches++
|
|
105
|
+
}
|
|
106
|
+
if (matches / minLen > 0.7) score += 5
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { item, score }
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Filter and sort by score
|
|
115
|
+
filtered = scored
|
|
116
|
+
.filter(s => s.score > 0)
|
|
117
|
+
.sort((a, b) => b.score - a.score)
|
|
118
|
+
.slice(0, 30)
|
|
119
|
+
.map(s => s.item)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Group by type
|
|
123
|
+
const grouped: GroupedResults = {
|
|
124
|
+
docs: filtered.filter(i => i.type === 'doc'),
|
|
125
|
+
endpoints: filtered.filter(i => i.type === 'endpoint'),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { groupedResults: grouped }
|
|
129
|
+
}, [query, items])
|
|
130
|
+
|
|
131
|
+
// Flat list for keyboard navigation
|
|
132
|
+
const flatItems = useMemo(() => {
|
|
133
|
+
return [...groupedResults.docs, ...groupedResults.endpoints]
|
|
134
|
+
}, [groupedResults])
|
|
135
|
+
|
|
136
|
+
// Reset selection when results change
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
setSelectedIndex(0)
|
|
139
|
+
}, [flatItems])
|
|
140
|
+
|
|
141
|
+
// Reset and focus when dialog opens
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (open) {
|
|
144
|
+
setQuery('')
|
|
145
|
+
setSelectedIndex(0)
|
|
146
|
+
setTimeout(() => inputRef.current?.focus(), 50)
|
|
147
|
+
}
|
|
148
|
+
}, [open])
|
|
149
|
+
|
|
150
|
+
// Scroll selected item into view
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (listRef.current && flatItems.length > 0) {
|
|
153
|
+
const selectedEl = listRef.current.querySelector(`[data-index="${selectedIndex}"]`)
|
|
154
|
+
selectedEl?.scrollIntoView({ block: 'nearest' })
|
|
155
|
+
}
|
|
156
|
+
}, [selectedIndex, flatItems.length])
|
|
157
|
+
|
|
158
|
+
// Handle selection
|
|
159
|
+
const handleSelect = useCallback((item: SearchItem) => {
|
|
160
|
+
onSelect(item)
|
|
161
|
+
onOpenChange(false)
|
|
162
|
+
}, [onSelect, onOpenChange])
|
|
163
|
+
|
|
164
|
+
// Keyboard navigation
|
|
165
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
166
|
+
switch (e.key) {
|
|
167
|
+
case 'ArrowDown':
|
|
168
|
+
e.preventDefault()
|
|
169
|
+
setSelectedIndex(i => Math.min(i + 1, flatItems.length - 1))
|
|
170
|
+
break
|
|
171
|
+
case 'ArrowUp':
|
|
172
|
+
e.preventDefault()
|
|
173
|
+
setSelectedIndex(i => Math.max(i - 1, 0))
|
|
174
|
+
break
|
|
175
|
+
case 'Enter':
|
|
176
|
+
e.preventDefault()
|
|
177
|
+
if (flatItems[selectedIndex]) {
|
|
178
|
+
handleSelect(flatItems[selectedIndex])
|
|
179
|
+
}
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
}, [flatItems, selectedIndex, handleSelect])
|
|
183
|
+
|
|
184
|
+
// Get the global index for an item
|
|
185
|
+
const getGlobalIndex = (item: SearchItem) => {
|
|
186
|
+
return flatItems.findIndex(i => i.id === item.id)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const renderItem = (item: SearchItem) => {
|
|
190
|
+
const index = getGlobalIndex(item)
|
|
191
|
+
return (
|
|
192
|
+
<button
|
|
193
|
+
key={item.id}
|
|
194
|
+
data-index={index}
|
|
195
|
+
onClick={() => handleSelect(item)}
|
|
196
|
+
onMouseEnter={() => setSelectedIndex(index)}
|
|
197
|
+
className={cn(
|
|
198
|
+
'w-full flex items-center gap-3 px-3 py-2 rounded-md text-left transition-colors duration-75',
|
|
199
|
+
index === selectedIndex
|
|
200
|
+
? 'bg-primary/10 text-foreground'
|
|
201
|
+
: 'hover:bg-muted/50'
|
|
202
|
+
)}
|
|
203
|
+
>
|
|
204
|
+
{/* Icon */}
|
|
205
|
+
<div className="shrink-0">
|
|
206
|
+
{item.type === 'endpoint' ? (
|
|
207
|
+
<Code className="h-4 w-4 text-muted-foreground" weight="bold" />
|
|
208
|
+
) : (
|
|
209
|
+
<File className="h-4 w-4 text-muted-foreground" weight="bold" />
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{/* Content */}
|
|
214
|
+
<div className="flex-1 min-w-0">
|
|
215
|
+
<div className="flex items-center gap-2">
|
|
216
|
+
{item.method && (
|
|
217
|
+
<span className={cn(
|
|
218
|
+
'px-1.5 py-0.5 rounded text-[10px] font-bold uppercase shrink-0',
|
|
219
|
+
METHOD_COLORS[item.method] || 'bg-gray-500/10 text-gray-600'
|
|
220
|
+
)}>
|
|
221
|
+
{item.method}
|
|
222
|
+
</span>
|
|
223
|
+
)}
|
|
224
|
+
<span className="font-medium truncate text-sm">{item.title}</span>
|
|
225
|
+
</div>
|
|
226
|
+
{(item.path || item.description) && (
|
|
227
|
+
<p className="text-xs text-muted-foreground truncate mt-0.5">
|
|
228
|
+
{item.type === 'endpoint' ? item.path : item.description || item.path}
|
|
229
|
+
</p>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{/* Arrow */}
|
|
234
|
+
{index === selectedIndex && (
|
|
235
|
+
<ArrowRight className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
236
|
+
)}
|
|
237
|
+
</button>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const hasResults = flatItems.length > 0
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
245
|
+
<DialogContent
|
|
246
|
+
className="max-w-2xl p-0 gap-0 overflow-hidden !duration-150"
|
|
247
|
+
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
248
|
+
>
|
|
249
|
+
<VisuallyHidden.Root>
|
|
250
|
+
<DialogTitle>Search documentation</DialogTitle>
|
|
251
|
+
</VisuallyHidden.Root>
|
|
252
|
+
|
|
253
|
+
{/* Search input */}
|
|
254
|
+
<div className="flex items-center border-b px-4 gap-3">
|
|
255
|
+
<MagnifyingGlass className="h-5 w-5 text-muted-foreground shrink-0" />
|
|
256
|
+
<input
|
|
257
|
+
ref={inputRef}
|
|
258
|
+
value={query}
|
|
259
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
260
|
+
onKeyDown={handleKeyDown}
|
|
261
|
+
placeholder="Search documentation and endpoints..."
|
|
262
|
+
className="flex-1 h-14 text-base bg-transparent outline-none placeholder:text-muted-foreground"
|
|
263
|
+
/>
|
|
264
|
+
<kbd className="hidden sm:inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
|
|
265
|
+
ESC
|
|
266
|
+
</kbd>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{/* Results */}
|
|
270
|
+
<div ref={listRef} className="max-h-[400px] overflow-y-auto">
|
|
271
|
+
{!hasResults && query ? (
|
|
272
|
+
<div className="py-12 text-center text-muted-foreground">
|
|
273
|
+
<MagnifyingGlass className="h-10 w-10 mx-auto mb-3 opacity-50" />
|
|
274
|
+
<p className="text-sm">No results found for "{query}"</p>
|
|
275
|
+
</div>
|
|
276
|
+
) : !hasResults ? (
|
|
277
|
+
<div className="py-12 text-center text-muted-foreground">
|
|
278
|
+
<p className="text-sm">Start typing to search...</p>
|
|
279
|
+
</div>
|
|
280
|
+
) : (
|
|
281
|
+
<div className="py-2">
|
|
282
|
+
{/* Documentation Section */}
|
|
283
|
+
{groupedResults.docs.length > 0 && (
|
|
284
|
+
<div className="mb-2">
|
|
285
|
+
<div className="flex items-center gap-2 px-4 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
|
286
|
+
<BookOpen className="h-3.5 w-3.5" weight="bold" />
|
|
287
|
+
Documentation
|
|
288
|
+
<span className="text-[10px] font-normal ml-auto">{groupedResults.docs.length}</span>
|
|
289
|
+
</div>
|
|
290
|
+
<div className="px-2 space-y-0.5">
|
|
291
|
+
{groupedResults.docs.map(renderItem)}
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
|
|
296
|
+
{/* API Endpoints Section */}
|
|
297
|
+
{groupedResults.endpoints.length > 0 && (
|
|
298
|
+
<div>
|
|
299
|
+
<div className="flex items-center gap-2 px-4 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
|
300
|
+
<Lightning className="h-3.5 w-3.5" weight="bold" />
|
|
301
|
+
API Endpoints
|
|
302
|
+
<span className="text-[10px] font-normal ml-auto">{groupedResults.endpoints.length}</span>
|
|
303
|
+
</div>
|
|
304
|
+
<div className="px-2 space-y-0.5">
|
|
305
|
+
{groupedResults.endpoints.map(renderItem)}
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
{/* Footer */}
|
|
314
|
+
<div className="border-t px-4 py-2 flex items-center gap-4 text-xs text-muted-foreground bg-muted/30">
|
|
315
|
+
<span className="flex items-center gap-1">
|
|
316
|
+
<kbd className="px-1.5 py-0.5 rounded border bg-background text-[10px]">↑↓</kbd>
|
|
317
|
+
navigate
|
|
318
|
+
</span>
|
|
319
|
+
<span className="flex items-center gap-1">
|
|
320
|
+
<kbd className="px-1.5 py-0.5 rounded border bg-background text-[10px]">↵</kbd>
|
|
321
|
+
select
|
|
322
|
+
</span>
|
|
323
|
+
<span className="flex items-center gap-1">
|
|
324
|
+
<kbd className="px-1.5 py-0.5 rounded border bg-background text-[10px]">esc</kbd>
|
|
325
|
+
close
|
|
326
|
+
</span>
|
|
327
|
+
</div>
|
|
328
|
+
</DialogContent>
|
|
329
|
+
</Dialog>
|
|
330
|
+
)
|
|
331
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
4
|
+
import type { BrainfishRESTRequest, BrainfishCollection, BrainfishDocGroup, BrainfishDocPage } from '@/lib/api-docs/types'
|
|
5
|
+
|
|
6
|
+
interface SearchItem {
|
|
7
|
+
id: string
|
|
8
|
+
title: string
|
|
9
|
+
description?: string
|
|
10
|
+
type: 'endpoint' | 'doc' | 'folder'
|
|
11
|
+
method?: string
|
|
12
|
+
path?: string
|
|
13
|
+
href: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface UseSearchProps {
|
|
17
|
+
requests: BrainfishRESTRequest[]
|
|
18
|
+
folders: BrainfishCollection[]
|
|
19
|
+
docGroups?: BrainfishDocGroup[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useSearch({ requests, folders, docGroups }: UseSearchProps) {
|
|
23
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
24
|
+
|
|
25
|
+
// Build search items from collection
|
|
26
|
+
const searchItems = useMemo(() => {
|
|
27
|
+
const items: SearchItem[] = []
|
|
28
|
+
|
|
29
|
+
// Add endpoints from requests
|
|
30
|
+
requests.forEach(request => {
|
|
31
|
+
items.push({
|
|
32
|
+
id: request.id,
|
|
33
|
+
title: request.name,
|
|
34
|
+
description: request.description || undefined,
|
|
35
|
+
type: 'endpoint',
|
|
36
|
+
method: request.method,
|
|
37
|
+
path: request.endpoint,
|
|
38
|
+
href: `#api/${request.id}`,
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// Add endpoints from folders recursively
|
|
43
|
+
const addFolderItems = (folder: BrainfishCollection, parentPath = '') => {
|
|
44
|
+
folder.requests?.forEach((request: BrainfishRESTRequest) => {
|
|
45
|
+
items.push({
|
|
46
|
+
id: request.id,
|
|
47
|
+
title: request.name,
|
|
48
|
+
description: request.description || undefined,
|
|
49
|
+
type: 'endpoint',
|
|
50
|
+
method: request.method,
|
|
51
|
+
path: request.endpoint,
|
|
52
|
+
href: `#api/${request.id}`,
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
folder.folders?.forEach((subFolder: BrainfishCollection) => {
|
|
57
|
+
addFolderItems(subFolder, `${parentPath}/${folder.name}`)
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
folders.forEach(folder => addFolderItems(folder))
|
|
62
|
+
|
|
63
|
+
// Add doc pages recursively (including nested children)
|
|
64
|
+
const addDocPages = (pages: BrainfishDocPage[], groupPath: string) => {
|
|
65
|
+
pages.forEach(page => {
|
|
66
|
+
items.push({
|
|
67
|
+
id: `doc-${page.slug}`,
|
|
68
|
+
title: page.title,
|
|
69
|
+
description: page.description,
|
|
70
|
+
type: 'doc',
|
|
71
|
+
path: groupPath,
|
|
72
|
+
href: `#guides/page/${page.slug}`,
|
|
73
|
+
})
|
|
74
|
+
// Recursively add children
|
|
75
|
+
if (page.children && page.children.length > 0) {
|
|
76
|
+
addDocPages(page.children, `${groupPath} / ${page.title}`)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Add doc groups recursively
|
|
82
|
+
const addDocGroup = (group: BrainfishDocGroup, parentPath = '') => {
|
|
83
|
+
const groupPath = parentPath ? `${parentPath} / ${group.title}` : group.title
|
|
84
|
+
addDocPages(group.pages, groupPath)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
docGroups?.forEach(group => {
|
|
88
|
+
addDocGroup(group)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
return items
|
|
92
|
+
}, [requests, folders, docGroups])
|
|
93
|
+
|
|
94
|
+
// Global keyboard shortcut (Cmd+K or Ctrl+K)
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
97
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
98
|
+
e.preventDefault()
|
|
99
|
+
setIsOpen(true)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
104
|
+
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
105
|
+
}, [])
|
|
106
|
+
|
|
107
|
+
const openSearch = useCallback(() => setIsOpen(true), [])
|
|
108
|
+
const closeSearch = useCallback(() => setIsOpen(false), [])
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
isOpen,
|
|
112
|
+
setIsOpen,
|
|
113
|
+
openSearch,
|
|
114
|
+
closeSearch,
|
|
115
|
+
searchItems,
|
|
116
|
+
}
|
|
117
|
+
}
|