skrypt-ai 0.6.1 → 0.8.0

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 (180) hide show
  1. package/dist/audit/doc-parser.d.ts +5 -0
  2. package/dist/audit/doc-parser.js +106 -0
  3. package/dist/audit/index.d.ts +4 -0
  4. package/dist/audit/index.js +4 -0
  5. package/dist/audit/matcher.d.ts +6 -0
  6. package/dist/audit/matcher.js +94 -0
  7. package/dist/audit/reporter.d.ts +9 -0
  8. package/dist/audit/reporter.js +106 -0
  9. package/dist/audit/types.d.ts +37 -0
  10. package/dist/auth/index.js +6 -4
  11. package/dist/cli.js +12 -2
  12. package/dist/commands/audit.d.ts +2 -0
  13. package/dist/commands/audit.js +59 -0
  14. package/dist/commands/config.d.ts +2 -0
  15. package/dist/commands/config.js +73 -0
  16. package/dist/commands/{generate.d.ts → generate/index.d.ts} +1 -0
  17. package/dist/commands/generate/index.js +393 -0
  18. package/dist/commands/generate/scan.d.ts +41 -0
  19. package/dist/commands/generate/scan.js +256 -0
  20. package/dist/commands/generate/verify.d.ts +14 -0
  21. package/dist/commands/generate/verify.js +122 -0
  22. package/dist/commands/generate/write.d.ts +25 -0
  23. package/dist/commands/generate/write.js +120 -0
  24. package/dist/commands/import.js +4 -1
  25. package/dist/commands/llms-txt.js +6 -4
  26. package/dist/commands/refresh.d.ts +2 -0
  27. package/dist/commands/refresh.js +158 -0
  28. package/dist/commands/review.d.ts +2 -0
  29. package/dist/commands/review.js +110 -0
  30. package/dist/commands/test.js +177 -236
  31. package/dist/commands/watch.js +29 -20
  32. package/dist/config/loader.d.ts +6 -2
  33. package/dist/config/loader.js +39 -3
  34. package/dist/config/types.d.ts +7 -0
  35. package/dist/generator/agents-md.d.ts +25 -0
  36. package/dist/generator/agents-md.js +122 -0
  37. package/dist/generator/generator.js +2 -1
  38. package/dist/generator/index.d.ts +2 -0
  39. package/dist/generator/index.js +2 -0
  40. package/dist/generator/mdx-serializer.d.ts +11 -0
  41. package/dist/generator/mdx-serializer.js +135 -0
  42. package/dist/generator/organizer.d.ts +1 -16
  43. package/dist/generator/organizer.js +0 -38
  44. package/dist/generator/types.d.ts +3 -0
  45. package/dist/generator/writer.js +65 -32
  46. package/dist/github/org-discovery.d.ts +17 -0
  47. package/dist/github/org-discovery.js +93 -0
  48. package/dist/llm/index.d.ts +2 -0
  49. package/dist/llm/index.js +8 -2
  50. package/dist/llm/proxy-client.d.ts +32 -0
  51. package/dist/llm/proxy-client.js +103 -0
  52. package/dist/next-actions/actions.d.ts +2 -0
  53. package/dist/next-actions/actions.js +190 -0
  54. package/dist/next-actions/index.d.ts +6 -0
  55. package/dist/next-actions/index.js +39 -0
  56. package/dist/next-actions/setup.d.ts +2 -0
  57. package/dist/next-actions/setup.js +72 -0
  58. package/dist/next-actions/state.d.ts +7 -0
  59. package/dist/next-actions/state.js +68 -0
  60. package/dist/next-actions/suggest.d.ts +3 -0
  61. package/dist/next-actions/suggest.js +47 -0
  62. package/dist/next-actions/types.d.ts +26 -0
  63. package/dist/refresh/differ.d.ts +9 -0
  64. package/dist/refresh/differ.js +67 -0
  65. package/dist/refresh/index.d.ts +4 -0
  66. package/dist/refresh/index.js +4 -0
  67. package/dist/refresh/manifest.d.ts +18 -0
  68. package/dist/refresh/manifest.js +71 -0
  69. package/dist/refresh/splicer.d.ts +9 -0
  70. package/dist/refresh/splicer.js +50 -0
  71. package/dist/refresh/types.d.ts +37 -0
  72. package/dist/review/index.d.ts +8 -0
  73. package/dist/review/index.js +94 -0
  74. package/dist/review/parser.d.ts +16 -0
  75. package/dist/review/parser.js +95 -0
  76. package/dist/review/types.d.ts +18 -0
  77. package/dist/scanner/csharp.d.ts +0 -4
  78. package/dist/scanner/csharp.js +9 -49
  79. package/dist/scanner/go.d.ts +0 -3
  80. package/dist/scanner/go.js +8 -35
  81. package/dist/scanner/java.d.ts +0 -4
  82. package/dist/scanner/java.js +9 -49
  83. package/dist/scanner/kotlin.d.ts +0 -3
  84. package/dist/scanner/kotlin.js +6 -33
  85. package/dist/scanner/php.d.ts +0 -10
  86. package/dist/scanner/php.js +11 -55
  87. package/dist/scanner/ruby.d.ts +0 -3
  88. package/dist/scanner/ruby.js +8 -38
  89. package/dist/scanner/rust.d.ts +0 -3
  90. package/dist/scanner/rust.js +10 -37
  91. package/dist/scanner/swift.d.ts +0 -3
  92. package/dist/scanner/swift.js +8 -35
  93. package/dist/scanner/types.d.ts +2 -0
  94. package/dist/scanner/utils.d.ts +41 -0
  95. package/dist/scanner/utils.js +97 -0
  96. package/dist/structure/index.d.ts +19 -0
  97. package/dist/structure/index.js +92 -0
  98. package/dist/structure/planner.d.ts +8 -0
  99. package/dist/structure/planner.js +180 -0
  100. package/dist/structure/topology.d.ts +16 -0
  101. package/dist/structure/topology.js +49 -0
  102. package/dist/structure/types.d.ts +26 -0
  103. package/dist/template/docs.json +5 -2
  104. package/dist/template/next.config.mjs +31 -0
  105. package/dist/template/package.json +5 -3
  106. package/dist/template/src/app/layout.tsx +13 -13
  107. package/dist/template/src/app/llms-full.md/route.ts +29 -0
  108. package/dist/template/src/app/llms.txt/route.ts +29 -0
  109. package/dist/template/src/app/md/[...slug]/route.ts +174 -0
  110. package/dist/template/src/app/reference/route.ts +22 -18
  111. package/dist/template/src/app/sitemap.ts +1 -1
  112. package/dist/template/src/components/ai-chat-impl.tsx +206 -0
  113. package/dist/template/src/components/ai-chat.tsx +20 -193
  114. package/dist/template/src/components/mdx/index.tsx +27 -4
  115. package/dist/template/src/lib/fonts.ts +135 -0
  116. package/dist/template/src/middleware.ts +101 -0
  117. package/dist/template/src/styles/globals.css +28 -20
  118. package/dist/testing/comparator.d.ts +7 -0
  119. package/dist/testing/comparator.js +77 -0
  120. package/dist/testing/docker.d.ts +21 -0
  121. package/dist/testing/docker.js +234 -0
  122. package/dist/testing/env.d.ts +16 -0
  123. package/dist/testing/env.js +58 -0
  124. package/dist/testing/extractor.d.ts +9 -0
  125. package/dist/testing/extractor.js +195 -0
  126. package/dist/testing/index.d.ts +6 -0
  127. package/dist/testing/index.js +6 -0
  128. package/dist/testing/runner.d.ts +5 -0
  129. package/dist/testing/runner.js +225 -0
  130. package/dist/testing/types.d.ts +58 -0
  131. package/dist/utils/files.d.ts +0 -8
  132. package/dist/utils/files.js +0 -33
  133. package/package.json +1 -1
  134. package/dist/autofix/autofix.test.js +0 -487
  135. package/dist/commands/generate.js +0 -445
  136. package/dist/generator/generator.test.js +0 -259
  137. package/dist/generator/writer.test.js +0 -411
  138. package/dist/llm/llm.manual-test.js +0 -112
  139. package/dist/llm/llm.mock-test.d.ts +0 -4
  140. package/dist/llm/llm.mock-test.js +0 -79
  141. package/dist/plugins/index.d.ts +0 -47
  142. package/dist/plugins/index.js +0 -181
  143. package/dist/scanner/content-type.test.js +0 -231
  144. package/dist/scanner/integration.test.d.ts +0 -4
  145. package/dist/scanner/integration.test.js +0 -180
  146. package/dist/scanner/scanner.test.js +0 -210
  147. package/dist/scanner/typescript.manual-test.d.ts +0 -1
  148. package/dist/scanner/typescript.manual-test.js +0 -112
  149. package/dist/template/src/app/docs/auth/page.mdx +0 -589
  150. package/dist/template/src/app/docs/autofix/page.mdx +0 -624
  151. package/dist/template/src/app/docs/cli/page.mdx +0 -217
  152. package/dist/template/src/app/docs/config/page.mdx +0 -428
  153. package/dist/template/src/app/docs/configuration/page.mdx +0 -86
  154. package/dist/template/src/app/docs/deployment/page.mdx +0 -112
  155. package/dist/template/src/app/docs/generator/generator.md +0 -504
  156. package/dist/template/src/app/docs/generator/organizer.md +0 -779
  157. package/dist/template/src/app/docs/generator/page.mdx +0 -613
  158. package/dist/template/src/app/docs/github/page.mdx +0 -502
  159. package/dist/template/src/app/docs/llm/anthropic-client.md +0 -549
  160. package/dist/template/src/app/docs/llm/index.md +0 -471
  161. package/dist/template/src/app/docs/llm/page.mdx +0 -428
  162. package/dist/template/src/app/docs/plugins/page.mdx +0 -1793
  163. package/dist/template/src/app/docs/pro/page.mdx +0 -121
  164. package/dist/template/src/app/docs/quickstart/page.mdx +0 -93
  165. package/dist/template/src/app/docs/scanner/content-type.md +0 -599
  166. package/dist/template/src/app/docs/scanner/index.md +0 -212
  167. package/dist/template/src/app/docs/scanner/page.mdx +0 -307
  168. package/dist/template/src/app/docs/scanner/python.md +0 -469
  169. package/dist/template/src/app/docs/scanner/python_parser.md +0 -1056
  170. package/dist/template/src/app/docs/scanner/rust.md +0 -325
  171. package/dist/template/src/app/docs/scanner/typescript.md +0 -201
  172. package/dist/template/src/app/icon.tsx +0 -29
  173. package/dist/utils/validation.d.ts +0 -1
  174. package/dist/utils/validation.js +0 -12
  175. /package/dist/{autofix/autofix.test.d.ts → audit/types.js} +0 -0
  176. /package/dist/{generator/generator.test.d.ts → next-actions/types.js} +0 -0
  177. /package/dist/{generator/writer.test.d.ts → refresh/types.js} +0 -0
  178. /package/dist/{llm/llm.manual-test.d.ts → review/types.js} +0 -0
  179. /package/dist/{scanner/content-type.test.d.ts → structure/types.js} +0 -0
  180. /package/dist/{scanner/scanner.test.d.ts → testing/types.js} +0 -0
@@ -1,5 +1,3 @@
1
- import { ApiReference } from '@scalar/nextjs-api-reference'
2
-
3
1
  const MOCK_SERVER_ENABLED = process.env.NEXT_PUBLIC_MOCK_SERVER === 'true'
4
2
 
5
3
  // Build servers list
@@ -18,19 +16,25 @@ const servers = [
18
16
  }] : []),
19
17
  ]
20
18
 
21
- export const GET = ApiReference({
22
- spec: {
23
- url: '/api/openapi',
24
- },
25
- // Scalar configuration options
26
- theme: 'default',
27
- hideModels: false,
28
- darkMode: true,
29
- servers,
30
- // Enable request sharing (#76)
31
- showSidebar: true,
32
- searchHotKey: 'k',
33
- metaData: {
34
- title: 'API Reference',
35
- },
36
- })
19
+ // Dynamic import @scalar/nextjs-api-reference is only loaded when
20
+ // this route is actually hit (not bundled into the main docs pages)
21
+ export const GET = async () => {
22
+ const { ApiReference } = await import('@scalar/nextjs-api-reference')
23
+ const handler = ApiReference({
24
+ spec: {
25
+ url: '/api/openapi',
26
+ },
27
+ // Scalar configuration options
28
+ theme: 'default',
29
+ hideModels: false,
30
+ darkMode: true,
31
+ servers,
32
+ // Enable request sharing (#76)
33
+ showSidebar: true,
34
+ searchHotKey: 'k',
35
+ metaData: {
36
+ title: 'API Reference',
37
+ },
38
+ })
39
+ return handler()
40
+ }
@@ -52,7 +52,7 @@ async function scanDir(dir: string, prefix: string): Promise<string[]> {
52
52
 
53
53
  if (entry.isDirectory()) {
54
54
  files.push(...await scanDir(join(dir, entry.name), path))
55
- } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {
55
+ } else if ((entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) && !entry.name.startsWith('llms') && !entry.name.startsWith('_') && entry.name !== 'README.md') {
56
56
  files.push(path)
57
57
  }
58
58
  }
@@ -0,0 +1,206 @@
1
+ 'use client'
2
+
3
+ import { useState, useRef, useEffect } from 'react'
4
+ import { useChat } from '@ai-sdk/react'
5
+ import { Streamdown } from 'streamdown'
6
+ import { MessageSquare, X, Send, ExternalLink } from 'lucide-react'
7
+
8
+ interface Citation {
9
+ title: string
10
+ path: string
11
+ snippet: string
12
+ }
13
+
14
+ interface AIChatProps {
15
+ projectName?: string
16
+ apiEndpoint?: string
17
+ placeholder?: string
18
+ }
19
+
20
+ export function AIChat({
21
+ projectName = 'this documentation',
22
+ apiEndpoint = '/api/chat',
23
+ placeholder = 'Ask a question...',
24
+ }: AIChatProps) {
25
+ const [isOpen, setIsOpen] = useState(false)
26
+ const [citations, setCitations] = useState<Map<string, Citation[]>>(new Map())
27
+ const messagesEndRef = useRef<HTMLDivElement>(null)
28
+
29
+ const { messages, input, handleInputChange, handleSubmit, status, error } = useChat({
30
+ api: apiEndpoint,
31
+ onResponse(response: Response) {
32
+ // Extract citations from response header
33
+ const citationsHeader = response.headers.get('X-Citations')
34
+ if (citationsHeader) {
35
+ try {
36
+ const parsed = JSON.parse(atob(citationsHeader)) as Citation[]
37
+ // Associate citations with the next assistant message
38
+ setCitations(prev => {
39
+ const next = new Map(prev)
40
+ // Use a temporary key that will be replaced when the message arrives
41
+ next.set('_pending', parsed)
42
+ return next
43
+ })
44
+ } catch {
45
+ // Skip invalid citations
46
+ }
47
+ }
48
+ },
49
+ onFinish(message: { id: string }) {
50
+ // Move pending citations to the actual message ID
51
+ setCitations(prev => {
52
+ const next = new Map(prev)
53
+ const pending = next.get('_pending')
54
+ if (pending) {
55
+ next.delete('_pending')
56
+ next.set(message.id, pending)
57
+ }
58
+ return next
59
+ })
60
+ },
61
+ })
62
+
63
+ const isStreaming = status === 'streaming'
64
+ const isLoading = status === 'submitted'
65
+
66
+ useEffect(() => {
67
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
68
+ }, [messages, isStreaming])
69
+
70
+ if (!isOpen) {
71
+ return (
72
+ <button
73
+ onClick={() => setIsOpen(true)}
74
+ className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-[var(--color-text)] text-[var(--color-bg)] shadow-lg transition-transform hover:scale-105"
75
+ aria-label="Open AI chat"
76
+ >
77
+ <MessageSquare className="h-6 w-6" />
78
+ </button>
79
+ )
80
+ }
81
+
82
+ return (
83
+ <div className="fixed bottom-6 right-6 z-50 flex h-[500px] w-[380px] flex-col overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-bg)] shadow-2xl">
84
+ {/* Header */}
85
+ <div className="flex items-center justify-between border-b border-[var(--color-border)] px-4 py-3">
86
+ <div className="flex items-center gap-2">
87
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-[var(--color-text)]">
88
+ <MessageSquare className="h-4 w-4 text-[var(--color-bg)]" />
89
+ </div>
90
+ <div>
91
+ <p className="text-sm font-medium text-[var(--color-text)]">Ask AI</p>
92
+ <p className="text-xs text-[var(--color-text-tertiary)]">About {projectName}</p>
93
+ </div>
94
+ </div>
95
+ <button
96
+ onClick={() => setIsOpen(false)}
97
+ className="rounded-lg p-1 text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-bg-secondary)] hover:text-[var(--color-text-secondary)]"
98
+ aria-label="Close chat"
99
+ >
100
+ <X className="h-5 w-5" />
101
+ </button>
102
+ </div>
103
+
104
+ {/* Messages */}
105
+ <div className="flex-1 overflow-y-auto p-4 space-y-4">
106
+ {messages.length === 0 && (
107
+ <div className="text-center text-sm text-[var(--color-text-tertiary)] py-8">
108
+ <p>Ask me anything about {projectName}.</p>
109
+ <p className="mt-2 text-xs">I&apos;ll search the docs and give you an answer with sources.</p>
110
+ </div>
111
+ )}
112
+
113
+ {messages.map((message) => (
114
+ <div
115
+ key={message.id}
116
+ className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
117
+ >
118
+ <div
119
+ className={`max-w-[85%] rounded-2xl px-4 py-2 text-sm ${
120
+ message.role === 'user'
121
+ ? 'bg-[var(--color-text)] text-[var(--color-bg)]'
122
+ : 'bg-[var(--color-bg-tertiary)] text-[var(--color-text)]'
123
+ }`}
124
+ >
125
+ {message.role === 'assistant' ? (
126
+ <div className="chat-markdown prose prose-sm max-w-none">
127
+ <Streamdown
128
+ shikiTheme={['github-light', 'github-dark']}
129
+ isAnimating={isStreaming && message.id === messages[messages.length - 1]?.id}
130
+ >
131
+ {message.content}
132
+ </Streamdown>
133
+ </div>
134
+ ) : (
135
+ <p className="whitespace-pre-wrap">{message.content}</p>
136
+ )}
137
+
138
+ {/* Citations */}
139
+ {citations.get(message.id) && (
140
+ <div className="mt-3 border-t border-[var(--color-border)] pt-2">
141
+ <p className="text-xs font-medium text-[var(--color-text-tertiary)] mb-1">Sources:</p>
142
+ <div className="space-y-1">
143
+ {citations.get(message.id)!.map((citation, j) => (
144
+ <a
145
+ key={j}
146
+ href={citation.path}
147
+ className="flex items-center gap-1 text-xs text-[var(--color-primary)] hover:underline"
148
+ >
149
+ <ExternalLink className="h-3 w-3" />
150
+ {citation.title}
151
+ </a>
152
+ ))}
153
+ </div>
154
+ </div>
155
+ )}
156
+ </div>
157
+ </div>
158
+ ))}
159
+
160
+ {isLoading && (
161
+ <div className="flex justify-start">
162
+ <div className="flex items-center gap-2 rounded-2xl bg-[var(--color-bg-tertiary)] px-4 py-2 text-sm text-[var(--color-text-tertiary)]">
163
+ <span className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
164
+ Searching docs...
165
+ </div>
166
+ </div>
167
+ )}
168
+
169
+ {error && (
170
+ <div className="flex justify-start">
171
+ <div className="rounded-2xl bg-red-50 px-4 py-2 text-sm text-red-600">
172
+ Something went wrong. Please try again.
173
+ </div>
174
+ </div>
175
+ )}
176
+
177
+ <div ref={messagesEndRef} />
178
+ </div>
179
+
180
+ {/* Input */}
181
+ <form
182
+ onSubmit={handleSubmit}
183
+ className="border-t border-[var(--color-border)] p-3"
184
+ >
185
+ <div className="flex items-center gap-2">
186
+ <input
187
+ type="text"
188
+ value={input}
189
+ onChange={handleInputChange}
190
+ placeholder={placeholder}
191
+ disabled={isStreaming || isLoading}
192
+ className="flex-1 rounded-xl border border-[var(--color-border)] bg-[var(--color-bg-secondary)] px-4 py-2 text-sm text-[var(--color-text)] outline-none transition-colors placeholder:text-[var(--color-text-tertiary)] focus:border-[var(--color-border-strong)]"
193
+ />
194
+ <button
195
+ type="submit"
196
+ disabled={!input.trim() || isStreaming || isLoading}
197
+ className="flex h-10 w-10 items-center justify-center rounded-xl bg-[var(--color-text)] text-[var(--color-bg)] transition-colors hover:opacity-90 disabled:opacity-50"
198
+ aria-label="Send message"
199
+ >
200
+ <Send className="h-4 w-4" />
201
+ </button>
202
+ </div>
203
+ </form>
204
+ </div>
205
+ )
206
+ }
@@ -1,15 +1,7 @@
1
1
  'use client'
2
2
 
3
- import { useState, useRef, useEffect } from 'react'
4
- import { useChat } from '@ai-sdk/react'
5
- import { Streamdown } from 'streamdown'
6
- import { MessageSquare, X, Send, ExternalLink } from 'lucide-react'
7
-
8
- interface Citation {
9
- title: string
10
- path: string
11
- snippet: string
12
- }
3
+ import dynamic from 'next/dynamic'
4
+ import { MessageSquare } from 'lucide-react'
13
5
 
14
6
  interface AIChatProps {
15
7
  projectName?: string
@@ -17,190 +9,25 @@ interface AIChatProps {
17
9
  placeholder?: string
18
10
  }
19
11
 
20
- export function AIChat({
21
- projectName = 'this documentation',
22
- apiEndpoint = '/api/chat',
23
- placeholder = 'Ask a question...',
24
- }: AIChatProps) {
25
- const [isOpen, setIsOpen] = useState(false)
26
- const [citations, setCitations] = useState<Map<string, Citation[]>>(new Map())
27
- const messagesEndRef = useRef<HTMLDivElement>(null)
28
-
29
- const { messages, input, handleInputChange, handleSubmit, status, error } = useChat({
30
- api: apiEndpoint,
31
- onResponse(response: Response) {
32
- // Extract citations from response header
33
- const citationsHeader = response.headers.get('X-Citations')
34
- if (citationsHeader) {
35
- try {
36
- const parsed = JSON.parse(atob(citationsHeader)) as Citation[]
37
- // Associate citations with the next assistant message
38
- setCitations(prev => {
39
- const next = new Map(prev)
40
- // Use a temporary key that will be replaced when the message arrives
41
- next.set('_pending', parsed)
42
- return next
43
- })
44
- } catch {
45
- // Skip invalid citations
46
- }
47
- }
48
- },
49
- onFinish(message: { id: string }) {
50
- // Move pending citations to the actual message ID
51
- setCitations(prev => {
52
- const next = new Map(prev)
53
- const pending = next.get('_pending')
54
- if (pending) {
55
- next.delete('_pending')
56
- next.set(message.id, pending)
57
- }
58
- return next
59
- })
60
- },
61
- })
62
-
63
- const isStreaming = status === 'streaming'
64
- const isLoading = status === 'submitted'
65
-
66
- useEffect(() => {
67
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
68
- }, [messages, isStreaming])
69
-
70
- if (!isOpen) {
71
- return (
72
- <button
73
- onClick={() => setIsOpen(true)}
74
- className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-[var(--color-text)] text-[var(--color-bg)] shadow-lg transition-transform hover:scale-105"
75
- aria-label="Open AI chat"
76
- >
77
- <MessageSquare className="h-6 w-6" />
78
- </button>
79
- )
80
- }
81
-
12
+ // Thin loading placeholder — just the FAB button (no heavy deps loaded yet)
13
+ function AIChatLoading() {
82
14
  return (
83
- <div className="fixed bottom-6 right-6 z-50 flex h-[500px] w-[380px] flex-col overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-bg)] shadow-2xl">
84
- {/* Header */}
85
- <div className="flex items-center justify-between border-b border-[var(--color-border)] px-4 py-3">
86
- <div className="flex items-center gap-2">
87
- <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-[var(--color-text)]">
88
- <MessageSquare className="h-4 w-4 text-[var(--color-bg)]" />
89
- </div>
90
- <div>
91
- <p className="text-sm font-medium text-[var(--color-text)]">Ask AI</p>
92
- <p className="text-xs text-[var(--color-text-tertiary)]">About {projectName}</p>
93
- </div>
94
- </div>
95
- <button
96
- onClick={() => setIsOpen(false)}
97
- className="rounded-lg p-1 text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-bg-secondary)] hover:text-[var(--color-text-secondary)]"
98
- aria-label="Close chat"
99
- >
100
- <X className="h-5 w-5" />
101
- </button>
102
- </div>
103
-
104
- {/* Messages */}
105
- <div className="flex-1 overflow-y-auto p-4 space-y-4">
106
- {messages.length === 0 && (
107
- <div className="text-center text-sm text-[var(--color-text-tertiary)] py-8">
108
- <p>Ask me anything about {projectName}.</p>
109
- <p className="mt-2 text-xs">I&apos;ll search the docs and give you an answer with sources.</p>
110
- </div>
111
- )}
112
-
113
- {messages.map((message) => (
114
- <div
115
- key={message.id}
116
- className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
117
- >
118
- <div
119
- className={`max-w-[85%] rounded-2xl px-4 py-2 text-sm ${
120
- message.role === 'user'
121
- ? 'bg-[var(--color-text)] text-[var(--color-bg)]'
122
- : 'bg-[var(--color-bg-tertiary)] text-[var(--color-text)]'
123
- }`}
124
- >
125
- {message.role === 'assistant' ? (
126
- <div className="chat-markdown prose prose-sm max-w-none">
127
- <Streamdown
128
- shikiTheme={['github-light', 'github-dark']}
129
- isAnimating={isStreaming && message.id === messages[messages.length - 1]?.id}
130
- >
131
- {message.content}
132
- </Streamdown>
133
- </div>
134
- ) : (
135
- <p className="whitespace-pre-wrap">{message.content}</p>
136
- )}
137
-
138
- {/* Citations */}
139
- {citations.get(message.id) && (
140
- <div className="mt-3 border-t border-[var(--color-border)] pt-2">
141
- <p className="text-xs font-medium text-[var(--color-text-tertiary)] mb-1">Sources:</p>
142
- <div className="space-y-1">
143
- {citations.get(message.id)!.map((citation, j) => (
144
- <a
145
- key={j}
146
- href={citation.path}
147
- className="flex items-center gap-1 text-xs text-[var(--color-primary)] hover:underline"
148
- >
149
- <ExternalLink className="h-3 w-3" />
150
- {citation.title}
151
- </a>
152
- ))}
153
- </div>
154
- </div>
155
- )}
156
- </div>
157
- </div>
158
- ))}
159
-
160
- {isLoading && (
161
- <div className="flex justify-start">
162
- <div className="flex items-center gap-2 rounded-2xl bg-[var(--color-bg-tertiary)] px-4 py-2 text-sm text-[var(--color-text-tertiary)]">
163
- <span className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
164
- Searching docs...
165
- </div>
166
- </div>
167
- )}
168
-
169
- {error && (
170
- <div className="flex justify-start">
171
- <div className="rounded-2xl bg-red-50 px-4 py-2 text-sm text-red-600">
172
- Something went wrong. Please try again.
173
- </div>
174
- </div>
175
- )}
15
+ <button
16
+ className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-[var(--color-text)] text-[var(--color-bg)] shadow-lg transition-transform hover:scale-105"
17
+ aria-label="Open AI chat"
18
+ >
19
+ <MessageSquare className="h-6 w-6" />
20
+ </button>
21
+ )
22
+ }
176
23
 
177
- <div ref={messagesEndRef} />
178
- </div>
24
+ // Dynamic import — @ai-sdk/react, streamdown, and the full chat UI
25
+ // are only downloaded when this component is rendered on a page
26
+ const AIChatImpl = dynamic(
27
+ () => import('./ai-chat-impl').then(m => ({ default: m.AIChat })),
28
+ { ssr: false, loading: () => <AIChatLoading /> },
29
+ )
179
30
 
180
- {/* Input */}
181
- <form
182
- onSubmit={handleSubmit}
183
- className="border-t border-[var(--color-border)] p-3"
184
- >
185
- <div className="flex items-center gap-2">
186
- <input
187
- type="text"
188
- value={input}
189
- onChange={handleInputChange}
190
- placeholder={placeholder}
191
- disabled={isStreaming || isLoading}
192
- className="flex-1 rounded-xl border border-[var(--color-border)] bg-[var(--color-bg-secondary)] px-4 py-2 text-sm text-[var(--color-text)] outline-none transition-colors placeholder:text-[var(--color-text-tertiary)] focus:border-[var(--color-border-strong)]"
193
- />
194
- <button
195
- type="submit"
196
- disabled={!input.trim() || isStreaming || isLoading}
197
- className="flex h-10 w-10 items-center justify-center rounded-xl bg-[var(--color-text)] text-[var(--color-bg)] transition-colors hover:opacity-90 disabled:opacity-50"
198
- aria-label="Send message"
199
- >
200
- <Send className="h-4 w-4" />
201
- </button>
202
- </div>
203
- </form>
204
- </div>
205
- )
31
+ export function AIChat(props: AIChatProps) {
32
+ return <AIChatImpl {...props} />
206
33
  }
@@ -1,3 +1,6 @@
1
+ import dynamic from 'next/dynamic'
2
+
3
+ // Lightweight components — static imports (always in bundle)
1
4
  export { Card, CardGroup } from './card'
2
5
  export { Tabs, TabList, Tab, TabPanel } from './tabs'
3
6
  export { CodeGroup } from './code-group'
@@ -6,9 +9,6 @@ export { HighlightedCode } from './highlighted-code'
6
9
  export { Callout, Info, Warning, Success, Error, Tip, Note } from './callout'
7
10
  export { Accordion, AccordionGroup } from './accordion'
8
11
  export { Steps, Step } from './steps'
9
- export { CodePlayground } from './code-playground'
10
- export { PythonPlayground } from './python-playground'
11
- export { GoPlayground } from './go-playground'
12
12
  export { H1, H2, H3, H4 } from './heading'
13
13
  export { ParamTable, Schema } from './param-table'
14
14
  export { Changelog, ChangelogEntry, Change } from './changelog'
@@ -17,5 +17,28 @@ export { Tooltip } from './tooltip'
17
17
  export { Frame } from './frame'
18
18
  export { DarkImage } from './dark-image'
19
19
  export { LinkPreview } from './link-preview'
20
- export { Mermaid } from './mermaid'
21
20
  export { Screenshot } from './screenshot'
21
+
22
+ // Heavy components — dynamic imports (code-split, loaded only when rendered)
23
+ // Each underlying component already has 'use client' and guards browser APIs
24
+ // behind useEffect/handlers, so SSR of the shell is safe.
25
+
26
+ // @codesandbox/sandpack-react (~5 MB)
27
+ export const CodePlayground = dynamic(
28
+ () => import('./code-playground').then(m => ({ default: m.CodePlayground })),
29
+ )
30
+
31
+ // Pyodide loaded at runtime via CDN, but the component itself is still heavy
32
+ export const PythonPlayground = dynamic(
33
+ () => import('./python-playground').then(m => ({ default: m.PythonPlayground })),
34
+ )
35
+
36
+ // Go playground (uses fetch to go.dev API)
37
+ export const GoPlayground = dynamic(
38
+ () => import('./go-playground').then(m => ({ default: m.GoPlayground })),
39
+ )
40
+
41
+ // mermaid (~2.5 MB)
42
+ export const Mermaid = dynamic(
43
+ () => import('./mermaid').then(m => ({ default: m.Mermaid })),
44
+ )
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Font configuration for the documentation template.
3
+ *
4
+ * Supports customizable Google Fonts via docs.json:
5
+ *
6
+ * {
7
+ * "fonts": {
8
+ * "sans": "Inter", // default
9
+ * "mono": "JetBrains Mono" // default
10
+ * }
11
+ * }
12
+ *
13
+ * Also supports the legacy `theme.font` key for backward compatibility.
14
+ */
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Curated font lists — only supported Google Fonts are allowed
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export const SUPPORTED_SANS_FONTS: Record<string, { weights: string }> = {
21
+ 'Inter': { weights: '400;500;600;700;800' },
22
+ 'Plus Jakarta Sans': { weights: '400;500;600;700;800' },
23
+ 'DM Sans': { weights: '400;500;600;700' },
24
+ 'Geist': { weights: '400;500;600;700;800' },
25
+ 'Outfit': { weights: '400;500;600;700;800' },
26
+ 'Space Grotesk': { weights: '400;500;600;700' },
27
+ 'Manrope': { weights: '400;500;600;700;800' },
28
+ 'Sora': { weights: '400;500;600;700;800' },
29
+ 'Instrument Sans': { weights: '400;500;600;700' },
30
+ 'Albert Sans': { weights: '400;500;600;700;800' },
31
+ }
32
+
33
+ export const SUPPORTED_MONO_FONTS: Record<string, { weights: string }> = {
34
+ 'JetBrains Mono': { weights: '400;500;600;700' },
35
+ 'Fira Code': { weights: '400;500;600;700' },
36
+ 'Source Code Pro': { weights: '400;500;600;700' },
37
+ 'IBM Plex Mono': { weights: '400;500;600;700' },
38
+ 'Geist Mono': { weights: '400;500;600;700' },
39
+ 'Space Mono': { weights: '400;700' },
40
+ 'Roboto Mono': { weights: '400;500;600;700' },
41
+ 'Inconsolata': { weights: '400;500;600;700' },
42
+ }
43
+
44
+ const DEFAULT_SANS = 'Inter'
45
+ const DEFAULT_MONO = 'JetBrains Mono'
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Types
49
+ // ---------------------------------------------------------------------------
50
+
51
+ export interface ResolvedFont {
52
+ /** The font name as it appears in CSS font-family */
53
+ name: string
54
+ /** Whether this font needs to be loaded from Google Fonts */
55
+ isCustom: boolean
56
+ /** 'sans' or 'mono' */
57
+ type: 'sans' | 'mono'
58
+ }
59
+
60
+ export interface ResolvedFonts {
61
+ sans: ResolvedFont
62
+ mono: ResolvedFont
63
+ /** True if at least one font needs Google Fonts loading */
64
+ needsGoogleFonts: boolean
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Resolution
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /** Sanitize a font name — strip anything that isn't alphanumeric, space, or hyphen */
72
+ function sanitize(name: string): string {
73
+ return name.replace(/[^a-zA-Z0-9\s-]/g, '').trim()
74
+ }
75
+
76
+ /**
77
+ * Resolve font configuration from docs.json.
78
+ *
79
+ * Priority:
80
+ * 1. `fonts.sans` / `fonts.mono` (new format)
81
+ * 2. `theme.font` (legacy — applied to sans only)
82
+ * 3. Defaults: Inter + JetBrains Mono
83
+ *
84
+ * Unrecognized font names fall back to defaults.
85
+ */
86
+ export function resolveFonts(docsConfig: Record<string, unknown>): ResolvedFonts {
87
+ const fonts = docsConfig.fonts as { sans?: string; mono?: string } | undefined
88
+ const legacyFont = (docsConfig.theme as { font?: string } | undefined)?.font
89
+
90
+ // Resolve sans font
91
+ const rawSans = fonts?.sans || legacyFont || DEFAULT_SANS
92
+ const safeSans = sanitize(rawSans)
93
+ const sansIsSupported = safeSans in SUPPORTED_SANS_FONTS
94
+ const sansName = sansIsSupported ? safeSans : DEFAULT_SANS
95
+ const sansIsCustom = sansName !== DEFAULT_SANS
96
+
97
+ // Resolve mono font
98
+ const rawMono = fonts?.mono || DEFAULT_MONO
99
+ const safeMono = sanitize(rawMono)
100
+ const monoIsSupported = safeMono in SUPPORTED_MONO_FONTS
101
+ const monoName = monoIsSupported ? safeMono : DEFAULT_MONO
102
+ const monoIsCustom = monoName !== DEFAULT_MONO
103
+
104
+ return {
105
+ sans: { name: sansName, isCustom: sansIsCustom, type: 'sans' },
106
+ mono: { name: monoName, isCustom: monoIsCustom, type: 'mono' },
107
+ needsGoogleFonts: sansIsCustom || monoIsCustom,
108
+ }
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Google Fonts URL builder
113
+ // ---------------------------------------------------------------------------
114
+
115
+ /**
116
+ * Build a single Google Fonts CSS URL that loads both sans and mono fonts.
117
+ * Only includes fonts that are non-default (custom).
118
+ */
119
+ export function buildGoogleFontsUrl(sans: ResolvedFont, mono: ResolvedFont): string | null {
120
+ const families: string[] = []
121
+
122
+ if (sans.isCustom) {
123
+ const weights = SUPPORTED_SANS_FONTS[sans.name]?.weights || '400;500;600;700'
124
+ families.push(`family=${encodeURIComponent(sans.name)}:wght@${weights}`)
125
+ }
126
+
127
+ if (mono.isCustom) {
128
+ const weights = SUPPORTED_MONO_FONTS[mono.name]?.weights || '400;500;600;700'
129
+ families.push(`family=${encodeURIComponent(mono.name)}:wght@${weights}`)
130
+ }
131
+
132
+ if (families.length === 0) return null
133
+
134
+ return `https://fonts.googleapis.com/css2?${families.join('&')}&display=swap`
135
+ }