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.
- package/dist/audit/doc-parser.d.ts +5 -0
- package/dist/audit/doc-parser.js +106 -0
- package/dist/audit/index.d.ts +4 -0
- package/dist/audit/index.js +4 -0
- package/dist/audit/matcher.d.ts +6 -0
- package/dist/audit/matcher.js +94 -0
- package/dist/audit/reporter.d.ts +9 -0
- package/dist/audit/reporter.js +106 -0
- package/dist/audit/types.d.ts +37 -0
- package/dist/auth/index.js +6 -4
- package/dist/cli.js +12 -2
- package/dist/commands/audit.d.ts +2 -0
- package/dist/commands/audit.js +59 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +73 -0
- package/dist/commands/{generate.d.ts → generate/index.d.ts} +1 -0
- package/dist/commands/generate/index.js +393 -0
- package/dist/commands/generate/scan.d.ts +41 -0
- package/dist/commands/generate/scan.js +256 -0
- package/dist/commands/generate/verify.d.ts +14 -0
- package/dist/commands/generate/verify.js +122 -0
- package/dist/commands/generate/write.d.ts +25 -0
- package/dist/commands/generate/write.js +120 -0
- package/dist/commands/import.js +4 -1
- package/dist/commands/llms-txt.js +6 -4
- package/dist/commands/refresh.d.ts +2 -0
- package/dist/commands/refresh.js +158 -0
- package/dist/commands/review.d.ts +2 -0
- package/dist/commands/review.js +110 -0
- package/dist/commands/test.js +177 -236
- package/dist/commands/watch.js +29 -20
- package/dist/config/loader.d.ts +6 -2
- package/dist/config/loader.js +39 -3
- package/dist/config/types.d.ts +7 -0
- package/dist/generator/agents-md.d.ts +25 -0
- package/dist/generator/agents-md.js +122 -0
- package/dist/generator/generator.js +2 -1
- package/dist/generator/index.d.ts +2 -0
- package/dist/generator/index.js +2 -0
- package/dist/generator/mdx-serializer.d.ts +11 -0
- package/dist/generator/mdx-serializer.js +135 -0
- package/dist/generator/organizer.d.ts +1 -16
- package/dist/generator/organizer.js +0 -38
- package/dist/generator/types.d.ts +3 -0
- package/dist/generator/writer.js +65 -32
- package/dist/github/org-discovery.d.ts +17 -0
- package/dist/github/org-discovery.js +93 -0
- package/dist/llm/index.d.ts +2 -0
- package/dist/llm/index.js +8 -2
- package/dist/llm/proxy-client.d.ts +32 -0
- package/dist/llm/proxy-client.js +103 -0
- package/dist/next-actions/actions.d.ts +2 -0
- package/dist/next-actions/actions.js +190 -0
- package/dist/next-actions/index.d.ts +6 -0
- package/dist/next-actions/index.js +39 -0
- package/dist/next-actions/setup.d.ts +2 -0
- package/dist/next-actions/setup.js +72 -0
- package/dist/next-actions/state.d.ts +7 -0
- package/dist/next-actions/state.js +68 -0
- package/dist/next-actions/suggest.d.ts +3 -0
- package/dist/next-actions/suggest.js +47 -0
- package/dist/next-actions/types.d.ts +26 -0
- package/dist/refresh/differ.d.ts +9 -0
- package/dist/refresh/differ.js +67 -0
- package/dist/refresh/index.d.ts +4 -0
- package/dist/refresh/index.js +4 -0
- package/dist/refresh/manifest.d.ts +18 -0
- package/dist/refresh/manifest.js +71 -0
- package/dist/refresh/splicer.d.ts +9 -0
- package/dist/refresh/splicer.js +50 -0
- package/dist/refresh/types.d.ts +37 -0
- package/dist/review/index.d.ts +8 -0
- package/dist/review/index.js +94 -0
- package/dist/review/parser.d.ts +16 -0
- package/dist/review/parser.js +95 -0
- package/dist/review/types.d.ts +18 -0
- package/dist/scanner/csharp.d.ts +0 -4
- package/dist/scanner/csharp.js +9 -49
- package/dist/scanner/go.d.ts +0 -3
- package/dist/scanner/go.js +8 -35
- package/dist/scanner/java.d.ts +0 -4
- package/dist/scanner/java.js +9 -49
- package/dist/scanner/kotlin.d.ts +0 -3
- package/dist/scanner/kotlin.js +6 -33
- package/dist/scanner/php.d.ts +0 -10
- package/dist/scanner/php.js +11 -55
- package/dist/scanner/ruby.d.ts +0 -3
- package/dist/scanner/ruby.js +8 -38
- package/dist/scanner/rust.d.ts +0 -3
- package/dist/scanner/rust.js +10 -37
- package/dist/scanner/swift.d.ts +0 -3
- package/dist/scanner/swift.js +8 -35
- package/dist/scanner/types.d.ts +2 -0
- package/dist/scanner/utils.d.ts +41 -0
- package/dist/scanner/utils.js +97 -0
- package/dist/structure/index.d.ts +19 -0
- package/dist/structure/index.js +92 -0
- package/dist/structure/planner.d.ts +8 -0
- package/dist/structure/planner.js +180 -0
- package/dist/structure/topology.d.ts +16 -0
- package/dist/structure/topology.js +49 -0
- package/dist/structure/types.d.ts +26 -0
- package/dist/template/docs.json +5 -2
- package/dist/template/next.config.mjs +31 -0
- package/dist/template/package.json +5 -3
- package/dist/template/src/app/layout.tsx +13 -13
- package/dist/template/src/app/llms-full.md/route.ts +29 -0
- package/dist/template/src/app/llms.txt/route.ts +29 -0
- package/dist/template/src/app/md/[...slug]/route.ts +174 -0
- package/dist/template/src/app/reference/route.ts +22 -18
- package/dist/template/src/app/sitemap.ts +1 -1
- package/dist/template/src/components/ai-chat-impl.tsx +206 -0
- package/dist/template/src/components/ai-chat.tsx +20 -193
- package/dist/template/src/components/mdx/index.tsx +27 -4
- package/dist/template/src/lib/fonts.ts +135 -0
- package/dist/template/src/middleware.ts +101 -0
- package/dist/template/src/styles/globals.css +28 -20
- package/dist/testing/comparator.d.ts +7 -0
- package/dist/testing/comparator.js +77 -0
- package/dist/testing/docker.d.ts +21 -0
- package/dist/testing/docker.js +234 -0
- package/dist/testing/env.d.ts +16 -0
- package/dist/testing/env.js +58 -0
- package/dist/testing/extractor.d.ts +9 -0
- package/dist/testing/extractor.js +195 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +6 -0
- package/dist/testing/runner.d.ts +5 -0
- package/dist/testing/runner.js +225 -0
- package/dist/testing/types.d.ts +58 -0
- package/dist/utils/files.d.ts +0 -8
- package/dist/utils/files.js +0 -33
- package/package.json +1 -1
- package/dist/autofix/autofix.test.js +0 -487
- package/dist/commands/generate.js +0 -445
- package/dist/generator/generator.test.js +0 -259
- package/dist/generator/writer.test.js +0 -411
- package/dist/llm/llm.manual-test.js +0 -112
- package/dist/llm/llm.mock-test.d.ts +0 -4
- package/dist/llm/llm.mock-test.js +0 -79
- package/dist/plugins/index.d.ts +0 -47
- package/dist/plugins/index.js +0 -181
- package/dist/scanner/content-type.test.js +0 -231
- package/dist/scanner/integration.test.d.ts +0 -4
- package/dist/scanner/integration.test.js +0 -180
- package/dist/scanner/scanner.test.js +0 -210
- package/dist/scanner/typescript.manual-test.d.ts +0 -1
- package/dist/scanner/typescript.manual-test.js +0 -112
- package/dist/template/src/app/docs/auth/page.mdx +0 -589
- package/dist/template/src/app/docs/autofix/page.mdx +0 -624
- package/dist/template/src/app/docs/cli/page.mdx +0 -217
- package/dist/template/src/app/docs/config/page.mdx +0 -428
- package/dist/template/src/app/docs/configuration/page.mdx +0 -86
- package/dist/template/src/app/docs/deployment/page.mdx +0 -112
- package/dist/template/src/app/docs/generator/generator.md +0 -504
- package/dist/template/src/app/docs/generator/organizer.md +0 -779
- package/dist/template/src/app/docs/generator/page.mdx +0 -613
- package/dist/template/src/app/docs/github/page.mdx +0 -502
- package/dist/template/src/app/docs/llm/anthropic-client.md +0 -549
- package/dist/template/src/app/docs/llm/index.md +0 -471
- package/dist/template/src/app/docs/llm/page.mdx +0 -428
- package/dist/template/src/app/docs/plugins/page.mdx +0 -1793
- package/dist/template/src/app/docs/pro/page.mdx +0 -121
- package/dist/template/src/app/docs/quickstart/page.mdx +0 -93
- package/dist/template/src/app/docs/scanner/content-type.md +0 -599
- package/dist/template/src/app/docs/scanner/index.md +0 -212
- package/dist/template/src/app/docs/scanner/page.mdx +0 -307
- package/dist/template/src/app/docs/scanner/python.md +0 -469
- package/dist/template/src/app/docs/scanner/python_parser.md +0 -1056
- package/dist/template/src/app/docs/scanner/rust.md +0 -325
- package/dist/template/src/app/docs/scanner/typescript.md +0 -201
- package/dist/template/src/app/icon.tsx +0 -29
- package/dist/utils/validation.d.ts +0 -1
- package/dist/utils/validation.js +0 -12
- /package/dist/{autofix/autofix.test.d.ts → audit/types.js} +0 -0
- /package/dist/{generator/generator.test.d.ts → next-actions/types.js} +0 -0
- /package/dist/{generator/writer.test.d.ts → refresh/types.js} +0 -0
- /package/dist/{llm/llm.manual-test.d.ts → review/types.js} +0 -0
- /package/dist/{scanner/content-type.test.d.ts → structure/types.js} +0 -0
- /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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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'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
|
|
4
|
-
import {
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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'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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
+
}
|