@brainfish-ai/devdoc 0.1.43 → 0.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/cli/commands/create.js +2 -2
  2. package/package.json +1 -1
  3. package/renderer/app/api/collections/route.js +35 -4
  4. package/renderer/app/api/docs/route.js +9 -4
  5. package/renderer/app/api/suggestions/route.js +33 -13
  6. package/renderer/app/globals.css +69 -0
  7. package/renderer/app/layout.js +2 -2
  8. package/renderer/app/llms-full.txt/route.js +10 -1
  9. package/renderer/app/llms.txt/route.js +10 -1
  10. package/renderer/app/sitemap.xml/route.js +11 -1
  11. package/renderer/components/docs/mdx/cards.js +1 -1
  12. package/renderer/components/docs/mdx/landing.js +7 -5
  13. package/renderer/components/docs-viewer/agent/agent-chat.js +13 -112
  14. package/renderer/components/docs-viewer/agent/agent-popup-button.js +99 -0
  15. package/renderer/components/docs-viewer/agent/index.js +3 -0
  16. package/renderer/components/docs-viewer/content/content-router.js +182 -0
  17. package/renderer/components/docs-viewer/content/doc-page.js +73 -37
  18. package/renderer/components/docs-viewer/content/index.js +2 -0
  19. package/renderer/components/docs-viewer/content/mdx-error-boundary.js +184 -0
  20. package/renderer/components/docs-viewer/index.js +381 -485
  21. package/renderer/components/docs-viewer/playground/graphql-playground.js +205 -3
  22. package/renderer/components/docs-viewer/sidebar/right-sidebar.js +35 -39
  23. package/renderer/components/theme-toggle.js +1 -21
  24. package/renderer/hooks/use-route-state.js +159 -0
  25. package/renderer/lib/api-docs/agent/use-suggestions.js +97 -0
  26. package/renderer/lib/api-docs/code-editor/mode-context.js +61 -89
  27. package/renderer/lib/api-docs/mobile-context.js +40 -3
  28. package/renderer/lib/docs/config/environment.js +38 -0
  29. package/renderer/lib/docs/config/index.js +1 -0
  30. package/renderer/lib/docs/config/schema.js +17 -5
  31. package/renderer/lib/docs/mdx/compiler.js +5 -2
  32. package/renderer/lib/docs/mdx/index.js +2 -0
  33. package/renderer/lib/docs/mdx/remark-mermaid.js +63 -0
  34. package/renderer/lib/docs/navigation/index.js +1 -2
  35. package/renderer/lib/docs/navigation/types.js +3 -1
  36. package/renderer/lib/docs-navigation.js +140 -0
  37. package/renderer/package.json +1 -0
@@ -7,6 +7,7 @@ import matter from 'gray-matter';
7
7
  *
8
8
  * Generated from docs.json configuration.
9
9
  * Combines all documentation into a single file for LLM consumption.
10
+ * Private tabs and groups are excluded from the output.
10
11
  */ const DOCS_DIR = join(process.cwd(), 'templates', 'starter');
11
12
  const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
12
13
  function loadDocsConfig() {
@@ -72,6 +73,10 @@ function generateDocsContent(tab, lines) {
72
73
  lines.push('');
73
74
  if (tab.groups) {
74
75
  for (const group of tab.groups){
76
+ // Skip private groups
77
+ if (group.visibility === 'private') {
78
+ continue;
79
+ }
75
80
  lines.push(`## ${group.group}`);
76
81
  lines.push('');
77
82
  for (const page of group.pages){
@@ -201,9 +206,13 @@ function generateLlmsFullTxt(config) {
201
206
  lines.push('');
202
207
  lines.push('---');
203
208
  lines.push('');
204
- // Process each tab from config
209
+ // Process each tab from config (skip private tabs)
205
210
  if (config.navigation?.tabs) {
206
211
  for (const tab of config.navigation.tabs){
212
+ // Skip private tabs - they should not appear in LLM index
213
+ if (tab.visibility === 'private') {
214
+ continue;
215
+ }
207
216
  switch(tab.type){
208
217
  case 'docs':
209
218
  generateDocsContent(tab, lines);
@@ -7,6 +7,7 @@ import matter from 'gray-matter';
7
7
  *
8
8
  * Generated from docs.json configuration.
9
9
  * Helps LLMs understand and index your documentation structure.
10
+ * Private tabs and groups are excluded from the index.
10
11
  */ const DOCS_DIR = join(process.cwd(), 'templates', 'starter');
11
12
  const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
12
13
  function loadDocsConfig() {
@@ -73,6 +74,10 @@ function generateDocsSection(tab, lines) {
73
74
  lines.push('');
74
75
  if (tab.groups) {
75
76
  for (const group of tab.groups){
77
+ // Skip private groups
78
+ if (group.visibility === 'private') {
79
+ continue;
80
+ }
76
81
  lines.push(`### ${group.group}`);
77
82
  lines.push('');
78
83
  for (const page of group.pages){
@@ -163,9 +168,13 @@ function generateLlmsTxt(config) {
163
168
  lines.push('');
164
169
  lines.push('> Documentation index for LLMs. For full content, see /llms-full.txt');
165
170
  lines.push('');
166
- // Process each tab from config
171
+ // Process each tab from config (skip private tabs)
167
172
  if (config.navigation?.tabs) {
168
173
  for (const tab of config.navigation.tabs){
174
+ // Skip private tabs - they should not appear in LLM index
175
+ if (tab.visibility === 'private') {
176
+ continue;
177
+ }
169
178
  switch(tab.type){
170
179
  case 'docs':
171
180
  generateDocsSection(tab, lines);
@@ -5,6 +5,7 @@ import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
5
5
  * sitemap.xml - Auto-generated sitemap for search engines
6
6
  *
7
7
  * Generated from docs.json configuration.
8
+ * Private tabs and groups are excluded from the sitemap.
8
9
  */ const DOCS_DIR = join(process.cwd(), 'templates', 'starter');
9
10
  const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
10
11
  function loadDocsConfig() {
@@ -53,11 +54,19 @@ function generateSitemap(config) {
53
54
  changefreq: 'weekly',
54
55
  priority: 1.0
55
56
  });
56
- // Process navigation tabs
57
+ // Process navigation tabs (skip private tabs)
57
58
  if (config.navigation?.tabs) {
58
59
  for (const tab of config.navigation.tabs){
60
+ // Skip private tabs - they should not appear in sitemap
61
+ if (tab.visibility === 'private') {
62
+ continue;
63
+ }
59
64
  if (tab.type === 'docs' && tab.groups) {
60
65
  for (const group of tab.groups){
66
+ // Skip private groups
67
+ if (group.visibility === 'private') {
68
+ continue;
69
+ }
61
70
  for (const page of group.pages){
62
71
  const lastmod = getFileLastModified(page);
63
72
  const urlPath = page === 'index' ? '' : page;
@@ -93,6 +102,7 @@ function generateSitemap(config) {
93
102
  });
94
103
  }
95
104
  }
105
+ // Note: graphql tabs are skipped since they don't have individual pages
96
106
  }
97
107
  }
98
108
  // Generate XML
@@ -81,7 +81,7 @@ export function Card({ title, children, icon, iconSize = 'md', href, horizontal
81
81
  };
82
82
  const showArrow = arrow ?? isExternal;
83
83
  const cardContent = /*#__PURE__*/ _jsxs("div", {
84
- className: cn('docs-card group relative rounded-lg border border-border bg-card p-4 transition-all h-full', href && 'hover:border-primary/50 hover:shadow-md cursor-pointer', horizontal ? 'docs-card-horizontal flex items-start gap-4' : 'flex flex-col justify-start', className),
84
+ className: cn('docs-card group relative rounded-lg border border-border bg-transparent p-4 transition-all h-full', href && 'hover:border-primary/50 hover:bg-muted/30 cursor-pointer', horizontal ? 'docs-card-horizontal flex items-start gap-4' : 'flex flex-col justify-start', className),
85
85
  children: [
86
86
  img && !horizontal && /*#__PURE__*/ _jsx("div", {
87
87
  className: "mb-4 -mx-4 -mt-4 overflow-hidden rounded-t-lg",
@@ -12,13 +12,14 @@ const iconMap = {
12
12
  'arrow-up-right': ArrowUpRight,
13
13
  'rocket-launch': RocketLaunch
14
14
  };
15
- export function Hero({ children, className, variant = 'default', align = 'center', minHeight = 'md' }) {
15
+ export function Hero({ children, className, variant = 'transparent', align = 'center', minHeight = 'md' }) {
16
16
  const variantStyles = {
17
17
  default: 'bg-background text-foreground',
18
18
  gradient: 'bg-gradient-to-b from-background via-background to-muted/30 text-foreground',
19
19
  dark: 'bg-zinc-950 text-white [&_.landing-tagline]:text-zinc-400 [&_.landing-headline]:text-white [&_.landing-description]:text-zinc-300 [&_.landing-button]:border-zinc-600',
20
20
  light: 'bg-white text-zinc-900 [&_.landing-tagline]:text-zinc-500 [&_.landing-headline]:text-zinc-900 [&_.landing-description]:text-zinc-600 [&_.landing-button]:border-zinc-300',
21
- pattern: 'bg-background bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-primary/10 via-background to-background text-foreground'
21
+ pattern: 'bg-background bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-primary/10 via-background to-background text-foreground',
22
+ transparent: 'bg-transparent text-foreground'
22
23
  };
23
24
  const alignStyles = {
24
25
  top: 'items-start pt-12',
@@ -189,13 +190,14 @@ export function CommandBox({ command, prefix = '$', className, variant = 'defaul
189
190
  ]
190
191
  });
191
192
  }
192
- export function Section({ children, className, id, variant = 'default', padding = 'lg' }) {
193
+ export function Section({ children, className, id, variant = 'transparent', padding = 'lg' }) {
193
194
  const variantStyles = {
194
195
  default: 'bg-background text-foreground',
195
196
  muted: 'bg-muted/30 text-foreground',
196
197
  dark: 'bg-zinc-950 text-white [&_.landing-tagline]:text-zinc-400 [&_.landing-headline]:text-white [&_.landing-description]:text-zinc-300 [&_.landing-feature-title]:text-white [&_.landing-feature-description]:text-zinc-400 [&_.landing-button]:border-zinc-600',
197
198
  light: 'bg-white text-zinc-900 [&_.landing-tagline]:text-zinc-500 [&_.landing-headline]:text-zinc-900 [&_.landing-description]:text-zinc-600 [&_.landing-feature-title]:text-zinc-900 [&_.landing-feature-description]:text-zinc-600 [&_.landing-button]:border-zinc-300',
198
- accent: 'bg-primary/5 text-foreground'
199
+ accent: 'bg-primary/5 text-foreground',
200
+ transparent: 'bg-transparent text-foreground'
199
201
  };
200
202
  const paddingStyles = {
201
203
  none: '',
@@ -238,7 +240,7 @@ export function FeatureGrid({ children, className, cols = 3 }) {
238
240
  }
239
241
  export function FeatureItem({ children, title, icon, className }) {
240
242
  return /*#__PURE__*/ _jsxs("div", {
241
- className: cn('landing-feature-item p-6 rounded-xl border border-border/50 bg-card/50', 'hover:border-primary/30 hover:bg-card/80 transition-colors', className),
243
+ className: cn('landing-feature-item p-6 rounded-xl border border-border/50 bg-transparent', 'hover:border-primary/30 hover:bg-muted/30 transition-colors', className),
242
244
  children: [
243
245
  icon && /*#__PURE__*/ _jsx("div", {
244
246
  className: "landing-feature-icon mb-4 text-primary text-2xl",
@@ -12,6 +12,7 @@ import { cn } from '@/lib/utils';
12
12
  import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
13
13
  import { useModeContext, useNotes, useWorkspace } from '@/lib/api-docs/code-editor';
14
14
  import { useCurrentRequestPayload, useSendTrigger } from '@/lib/api-docs/playground/context';
15
+ import { useSuggestions } from '@/lib/api-docs/agent/use-suggestions';
15
16
  /**
16
17
  * Custom transport that intercepts streaming events for write_file tool
17
18
  * Wraps DefaultChatTransport and intercepts the stream to extract write_file content
@@ -126,35 +127,6 @@ import { useCurrentRequestPayload, useSendTrigger } from '@/lib/api-docs/playgro
126
127
  };
127
128
  return transport;
128
129
  }
129
- // Client-side cache for suggestions
130
- const SUGGESTIONS_CACHE_KEY = 'brainfish-suggestions-cache';
131
- function getSuggestionsFromCache(key) {
132
- try {
133
- const cached = localStorage.getItem(SUGGESTIONS_CACHE_KEY);
134
- if (cached) {
135
- const data = JSON.parse(cached);
136
- if (data[key] && Date.now() - data[key].timestamp < 1000 * 60 * 60) {
137
- return data[key].suggestions;
138
- }
139
- }
140
- } catch {
141
- // Ignore cache errors
142
- }
143
- return null;
144
- }
145
- function saveSuggestionsToCache(key, suggestions) {
146
- try {
147
- const cached = localStorage.getItem(SUGGESTIONS_CACHE_KEY);
148
- const data = cached ? JSON.parse(cached) : {};
149
- data[key] = {
150
- suggestions,
151
- timestamp: Date.now()
152
- };
153
- localStorage.setItem(SUGGESTIONS_CACHE_KEY, JSON.stringify(data));
154
- } catch {
155
- // Ignore cache errors
156
- }
157
- }
158
130
  // Storage key for chat history
159
131
  const CHAT_STORAGE_KEY = 'brainfish-agent-chat';
160
132
  export function AgentChat({ collection, currentEndpoint, onNavigate, onPrefill, apiSummary, debugContext, onDebugContextConsumed, explainContext, onExplainContextConsumed, onOpenGlobalAuth, onNavigateToAuthTab, onNavigateToParamsTab, onNavigateToBodyTab, onNavigateToHeadersTab, onNavigateToDocSection, onNavigateToDocPage, onHasMessagesChange }) {
@@ -172,18 +144,23 @@ export function AgentChat({ collection, currentEndpoint, onNavigate, onPrefill,
172
144
  const apiSpecUrl = process.env.NEXT_PUBLIC_OPENAPI_URL || collection.name || 'default';
173
145
  const { workspace } = useWorkspace(apiSpecUrl, collection.name);
174
146
  const { notes, createNote, updateNote, deleteNote, deleteAllNotes, refresh: refreshNotes } = useNotes(workspace?.id || null);
175
- // Suggestions state
176
- const [suggestions, setSuggestions] = useState([]);
177
- const [loadingSuggestions, setLoadingSuggestions] = useState(false);
147
+ // Build endpoint index for search
148
+ const endpointIndex = useMemo(()=>buildEndpointIndex(collection), [
149
+ collection
150
+ ]);
151
+ // Suggestions - use shared hook
152
+ const { suggestions, isLoading: loadingSuggestions } = useSuggestions({
153
+ currentEndpoint: currentEndpoint ? {
154
+ id: currentEndpoint.id,
155
+ name: currentEndpoint.name
156
+ } : null,
157
+ endpointIndex
158
+ });
178
159
  const [showSuggestions, setShowSuggestions] = useState(true);
179
160
  // Current request payload from playground
180
161
  const { currentRequestPayload } = useCurrentRequestPayload();
181
162
  // Send trigger - to invoke playground's send button
182
163
  const { requestSend } = useSendTrigger();
183
- // Build endpoint index for search
184
- const endpointIndex = useMemo(()=>buildEndpointIndex(collection), [
185
- collection
186
- ]);
187
164
  // Handler to open a file from chat (when user clicks on file name in tool display)
188
165
  const handleOpenFile = useCallback((path)=>{
189
166
  switchToNotes(path); // Pass path directly to avoid URL reset
@@ -1263,82 +1240,6 @@ export function AgentChat({ collection, currentEndpoint, onNavigate, onPrefill,
1263
1240
  }, [
1264
1241
  messages
1265
1242
  ]);
1266
- // Cache key for suggestions
1267
- const suggestionsCacheKey = currentEndpoint?.id ? `endpoint:${currentEndpoint.id}` : `general:${endpointIndex.length}`;
1268
- // Load dynamic suggestions from API
1269
- useEffect(()=>{
1270
- // Check cache first
1271
- const cached = getSuggestionsFromCache(suggestionsCacheKey);
1272
- if (cached) {
1273
- setSuggestions(cached);
1274
- return;
1275
- }
1276
- // Fetch from API
1277
- let cancelled = false;
1278
- setLoadingSuggestions(true);
1279
- fetch('/api/suggestions', {
1280
- method: 'POST',
1281
- headers: {
1282
- 'Content-Type': 'application/json'
1283
- },
1284
- body: JSON.stringify({
1285
- endpointIndex,
1286
- currentEndpointId: currentEndpoint?.id
1287
- })
1288
- }).then((res)=>res.json()).then((data)=>{
1289
- if (cancelled) return;
1290
- if (data.suggestions) {
1291
- setSuggestions(data.suggestions);
1292
- saveSuggestionsToCache(suggestionsCacheKey, data.suggestions);
1293
- }
1294
- }).catch((err)=>{
1295
- if (cancelled) return;
1296
- console.warn('[AgentChat] Failed to fetch suggestions:', err);
1297
- // Fallback suggestions
1298
- const fallback = currentEndpoint ? [
1299
- {
1300
- title: 'What does this',
1301
- label: 'endpoint do?',
1302
- prompt: `What does ${currentEndpoint.name} do?`
1303
- },
1304
- {
1305
- title: 'What parameters',
1306
- label: 'are required?',
1307
- prompt: 'What parameters are required?'
1308
- },
1309
- {
1310
- title: 'Python example',
1311
- label: 'Show code',
1312
- prompt: 'Show me a Python example'
1313
- }
1314
- ] : [
1315
- {
1316
- title: 'Find an endpoint',
1317
- label: 'to create resources',
1318
- prompt: 'Find endpoints for creating resources'
1319
- },
1320
- {
1321
- title: 'How do I',
1322
- label: 'authenticate?',
1323
- prompt: 'How do I authenticate?'
1324
- },
1325
- {
1326
- title: 'Overview',
1327
- label: 'What can I do?',
1328
- prompt: 'What can I do with this API?'
1329
- }
1330
- ];
1331
- setSuggestions(fallback);
1332
- }).finally(()=>{
1333
- if (!cancelled) setLoadingSuggestions(false);
1334
- });
1335
- return ()=>{
1336
- cancelled = true;
1337
- };
1338
- // eslint-disable-next-line react-hooks/exhaustive-deps
1339
- }, [
1340
- suggestionsCacheKey
1341
- ]);
1342
1243
  // Track pending debug error type for showing options after AI response
1343
1244
  const [pendingErrorType, setPendingErrorType] = useState(null);
1344
1245
  const prevStatus = useRef(status);
@@ -0,0 +1,99 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useState, useEffect } from 'react';
4
+ import { Sparkle, X } from '@phosphor-icons/react';
5
+ import { Button } from '@/components/ui/button';
6
+ import { cn } from '@/lib/utils';
7
+ import { useSuggestions } from '@/lib/api-docs/agent/use-suggestions';
8
+ export function AgentPopupButton({ onClick, onQuestionClick, className, currentEndpoint, endpointIndex = [] }) {
9
+ const [showSuggestions, setShowSuggestions] = useState(false);
10
+ const [isDismissed, setIsDismissed] = useState(false);
11
+ // Use shared suggestions hook - limit to 3 for popup
12
+ const { suggestions, isLoading } = useSuggestions({
13
+ currentEndpoint,
14
+ endpointIndex,
15
+ limit: 3
16
+ });
17
+ // Show suggestions after a delay (only if not dismissed and we have suggestions)
18
+ useEffect(()=>{
19
+ if (isDismissed || isLoading || suggestions.length === 0) return;
20
+ const timer = setTimeout(()=>{
21
+ setShowSuggestions(true);
22
+ }, 1500) // 1.5 second delay
23
+ ;
24
+ return ()=>clearTimeout(timer);
25
+ }, [
26
+ isDismissed,
27
+ isLoading,
28
+ suggestions.length
29
+ ]);
30
+ // Reset dismissed state when endpoint changes
31
+ useEffect(()=>{
32
+ setIsDismissed(false);
33
+ setShowSuggestions(false);
34
+ }, [
35
+ currentEndpoint?.id
36
+ ]);
37
+ const handleDismiss = (e)=>{
38
+ e.stopPropagation();
39
+ setShowSuggestions(false);
40
+ setIsDismissed(true);
41
+ };
42
+ const handleQuestionClick = (prompt)=>{
43
+ if (onQuestionClick) {
44
+ onQuestionClick(prompt);
45
+ } else {
46
+ onClick();
47
+ }
48
+ };
49
+ return /*#__PURE__*/ _jsxs("div", {
50
+ className: "fixed bottom-6 right-6 z-50 flex flex-col items-end gap-2",
51
+ children: [
52
+ showSuggestions && suggestions.length > 0 && /*#__PURE__*/ _jsxs(_Fragment, {
53
+ children: [
54
+ /*#__PURE__*/ _jsx("button", {
55
+ onClick: handleDismiss,
56
+ className: "size-6 rounded-full bg-background border border-border shadow-sm flex items-center justify-center hover:bg-muted transition-colors agent-suggestions-enter",
57
+ "aria-label": "Dismiss suggestions",
58
+ children: /*#__PURE__*/ _jsx(X, {
59
+ className: "size-3 text-muted-foreground",
60
+ weight: "bold"
61
+ })
62
+ }),
63
+ suggestions.map((suggestion, index)=>/*#__PURE__*/ _jsxs("button", {
64
+ onClick: ()=>handleQuestionClick(suggestion.prompt),
65
+ className: cn("text-sm px-4 py-2 rounded-full", "bg-background border border-border shadow-md", "hover:bg-muted hover:shadow-lg transition-all", "text-foreground", "agent-suggestion-item"),
66
+ style: {
67
+ animationDelay: `${(suggestions.length - index) * 80}ms`
68
+ },
69
+ children: [
70
+ /*#__PURE__*/ _jsx("span", {
71
+ className: "font-medium",
72
+ children: suggestion.title
73
+ }),
74
+ ' ',
75
+ /*#__PURE__*/ _jsx("span", {
76
+ className: "text-muted-foreground",
77
+ children: suggestion.label
78
+ })
79
+ ]
80
+ }, index))
81
+ ]
82
+ }),
83
+ /*#__PURE__*/ _jsxs(Button, {
84
+ onClick: onClick,
85
+ className: cn("h-10 px-4 rounded-full", "bg-primary hover:bg-primary/90 text-primary-foreground", "shadow-md shadow-primary/20 hover:shadow-lg hover:shadow-primary/30", "transition-all duration-200", "agent-popup-enter", "gap-2 font-medium", className),
86
+ "aria-label": "Open AI Assistant",
87
+ children: [
88
+ /*#__PURE__*/ _jsx(Sparkle, {
89
+ className: "size-4",
90
+ weight: "fill"
91
+ }),
92
+ /*#__PURE__*/ _jsx("span", {
93
+ children: "Ask AI"
94
+ })
95
+ ]
96
+ })
97
+ ]
98
+ });
99
+ }
@@ -2,3 +2,6 @@
2
2
  * Agent Components Exports
3
3
  */ export { AgentChat } from './agent-chat';
4
4
  export { ChatMessage, TypingIndicator } from './chat-message';
5
+ export { AgentPopupButton } from './agent-popup-button';
6
+ // Re-export shared hook and types
7
+ export { useSuggestions } from '@/lib/api-docs/agent/use-suggestions';
@@ -0,0 +1,182 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useMemo, useRef, useEffect } from 'react';
4
+ import { Book } from '@phosphor-icons/react';
5
+ import { cn } from '@/lib/utils';
6
+ import { RequestDetails } from './request-details';
7
+ import { DocPage } from './doc-page';
8
+ import { ChangelogPage } from './changelog-page';
9
+ import { NotFoundPage } from './not-found-page';
10
+ import { GraphQLPlayground } from '../playground/graphql-playground';
11
+ /**
12
+ * Find a request by ID in the collection
13
+ */ function findRequestById(collection, id) {
14
+ // Check direct requests
15
+ if (collection.requests) {
16
+ const found = collection.requests.find((r)=>r.id === id);
17
+ if (found) return found;
18
+ }
19
+ // Check in folders recursively
20
+ function searchFolders(folders) {
21
+ if (!folders) return null;
22
+ for (const folder of folders){
23
+ if (folder.requests) {
24
+ const found = folder.requests.find((r)=>r.id === id);
25
+ if (found) return found;
26
+ }
27
+ if (folder.folders) {
28
+ const found = searchFolders(folder.folders);
29
+ if (found) return found;
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+ return searchFolders(collection.folders);
35
+ }
36
+ /**
37
+ * ContentRouter - Renders content based on URL route state
38
+ *
39
+ * This component is the single place that determines what content to show
40
+ * based on the URL hash. It derives all state from the route.
41
+ */ export function ContentRouter({ routeState, collection, activeTabConfig, isGraphQLTab, isChangelogTab, graphqlOperations, graphqlSchemaSDL, graphqlEndpoint, onSearch, contentRef }) {
42
+ const { contentType, contentId } = routeState;
43
+ const localContentRef = useRef(null);
44
+ const actualRef = contentRef || localContentRef;
45
+ // Derive content from route state
46
+ const derivedContent = useMemo(()=>{
47
+ // GraphQL operation
48
+ if (isGraphQLTab && contentType === 'endpoint' && contentId) {
49
+ const operation = graphqlOperations.find((op)=>op.id === contentId);
50
+ if (operation) {
51
+ return {
52
+ type: 'graphql',
53
+ operation
54
+ };
55
+ }
56
+ }
57
+ // REST endpoint
58
+ if (contentType === 'endpoint' && contentId) {
59
+ const request = findRequestById(collection, contentId);
60
+ if (request) {
61
+ return {
62
+ type: 'endpoint',
63
+ request
64
+ };
65
+ }
66
+ // Endpoint not found
67
+ return {
68
+ type: 'not-found',
69
+ slug: `endpoint/${contentId}`
70
+ };
71
+ }
72
+ // Doc page
73
+ if (contentType === 'page' && contentId) {
74
+ return {
75
+ type: 'page',
76
+ slug: contentId
77
+ };
78
+ }
79
+ // Section scroll (handled elsewhere, show page if we have one)
80
+ if (contentType === 'section' && contentId) {
81
+ // Section navigation usually means scroll within current page
82
+ // For now, treat as a doc lookup
83
+ return {
84
+ type: 'section',
85
+ sectionId: contentId
86
+ };
87
+ }
88
+ // Changelog tab
89
+ if (isChangelogTab) {
90
+ return {
91
+ type: 'changelog'
92
+ };
93
+ }
94
+ // Default empty state
95
+ return {
96
+ type: 'empty'
97
+ };
98
+ }, [
99
+ contentType,
100
+ contentId,
101
+ isGraphQLTab,
102
+ isChangelogTab,
103
+ graphqlOperations,
104
+ collection
105
+ ]);
106
+ // Handle section scrolling
107
+ useEffect(()=>{
108
+ if (derivedContent.type === 'section' && 'sectionId' in derivedContent) {
109
+ const element = document.getElementById(derivedContent.sectionId);
110
+ if (element) {
111
+ setTimeout(()=>{
112
+ element.scrollIntoView({
113
+ behavior: 'smooth',
114
+ block: 'start'
115
+ });
116
+ }, 100);
117
+ }
118
+ }
119
+ }, [
120
+ derivedContent
121
+ ]);
122
+ // Determine if content needs flex layout (for playgrounds)
123
+ const needsFlexLayout = derivedContent.type === 'graphql' || derivedContent.type === 'changelog';
124
+ return /*#__PURE__*/ _jsx("div", {
125
+ ref: actualRef,
126
+ className: cn("docs-content-area flex-1 bg-background min-w-0", needsFlexLayout ? "overflow-hidden flex flex-col" : "overflow-y-auto scroll-smooth"),
127
+ children: derivedContent.type === 'graphql' && 'operation' in derivedContent ? /*#__PURE__*/ _jsx("div", {
128
+ className: "flex-1 flex flex-col h-full",
129
+ children: /*#__PURE__*/ _jsx(GraphQLPlayground, {
130
+ endpoint: graphqlEndpoint || '',
131
+ defaultQuery: derivedContent.operation.query,
132
+ operations: graphqlOperations,
133
+ selectedOperationId: derivedContent.operation.id,
134
+ hideExplorer: true,
135
+ headers: {},
136
+ theme: "dark",
137
+ schemaSDL: graphqlSchemaSDL
138
+ })
139
+ }) : derivedContent.type === 'changelog' ? /*#__PURE__*/ _jsx(ChangelogPage, {
140
+ releases: collection.changelogReleases || [],
141
+ tabName: activeTabConfig?.tab
142
+ }) : derivedContent.type === 'endpoint' && 'request' in derivedContent ? /*#__PURE__*/ _jsx(RequestDetails, {
143
+ request: derivedContent.request
144
+ }) : derivedContent.type === 'page' && 'slug' in derivedContent ? /*#__PURE__*/ _jsx(DocPage, {
145
+ slug: derivedContent.slug,
146
+ onSearch: onSearch
147
+ }) : derivedContent.type === 'not-found' && 'slug' in derivedContent ? /*#__PURE__*/ _jsx(NotFoundPage, {
148
+ slug: derivedContent.slug,
149
+ onSearch: onSearch
150
+ }) : derivedContent.type === 'section' ? // Section navigation - show empty state while scrolling to section
151
+ /*#__PURE__*/ _jsx("div", {
152
+ className: "flex-1 flex items-center justify-center bg-background",
153
+ children: /*#__PURE__*/ _jsxs("div", {
154
+ className: "text-center text-muted-foreground",
155
+ children: [
156
+ /*#__PURE__*/ _jsx(Book, {
157
+ className: "h-12 w-12 mx-auto mb-3 opacity-40"
158
+ }),
159
+ /*#__PURE__*/ _jsx("p", {
160
+ className: "text-sm",
161
+ children: "Scrolling to section..."
162
+ })
163
+ ]
164
+ })
165
+ }) : /*#__PURE__*/ _jsx("div", {
166
+ className: "flex-1 flex items-center justify-center bg-background",
167
+ children: /*#__PURE__*/ _jsxs("div", {
168
+ className: "text-center text-muted-foreground",
169
+ children: [
170
+ /*#__PURE__*/ _jsx(Book, {
171
+ className: "h-12 w-12 mx-auto mb-3 opacity-40"
172
+ }),
173
+ /*#__PURE__*/ _jsx("p", {
174
+ className: "text-sm",
175
+ children: "Select an endpoint from the sidebar"
176
+ })
177
+ ]
178
+ })
179
+ })
180
+ });
181
+ }
182
+ export { findRequestById };