skrypt-ai 0.1.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/LICENSE +21 -0
- package/README.md +200 -0
- package/dist/autofix/index.d.ts +46 -0
- package/dist/autofix/index.js +240 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +40 -0
- package/dist/commands/autofix.d.ts +2 -0
- package/dist/commands/autofix.js +143 -0
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.js +320 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +56 -0
- package/dist/commands/review-pr.d.ts +2 -0
- package/dist/commands/review-pr.js +117 -0
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +142 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +2 -0
- package/dist/config/loader.d.ts +9 -0
- package/dist/config/loader.js +82 -0
- package/dist/config/types.d.ts +24 -0
- package/dist/config/types.js +34 -0
- package/dist/generator/generator.d.ts +15 -0
- package/dist/generator/generator.js +144 -0
- package/dist/generator/index.d.ts +4 -0
- package/dist/generator/index.js +4 -0
- package/dist/generator/organizer.d.ts +29 -0
- package/dist/generator/organizer.js +222 -0
- package/dist/generator/types.d.ts +83 -0
- package/dist/generator/types.js +1 -0
- package/dist/generator/writer.d.ts +28 -0
- package/dist/generator/writer.js +320 -0
- package/dist/github/pr-comments.d.ts +40 -0
- package/dist/github/pr-comments.js +308 -0
- package/dist/llm/anthropic-client.d.ts +16 -0
- package/dist/llm/anthropic-client.js +92 -0
- package/dist/llm/index.d.ts +53 -0
- package/dist/llm/index.js +400 -0
- package/dist/llm/llm.manual-test.d.ts +1 -0
- package/dist/llm/llm.manual-test.js +112 -0
- package/dist/llm/llm.mock-test.d.ts +4 -0
- package/dist/llm/llm.mock-test.js +79 -0
- package/dist/llm/openai-client.d.ts +17 -0
- package/dist/llm/openai-client.js +90 -0
- package/dist/llm/types.d.ts +60 -0
- package/dist/llm/types.js +20 -0
- package/dist/scanner/content-type.d.ts +39 -0
- package/dist/scanner/content-type.js +194 -0
- package/dist/scanner/content-type.test.d.ts +1 -0
- package/dist/scanner/content-type.test.js +231 -0
- package/dist/scanner/go.d.ts +20 -0
- package/dist/scanner/go.js +269 -0
- package/dist/scanner/index.d.ts +21 -0
- package/dist/scanner/index.js +137 -0
- package/dist/scanner/python.d.ts +6 -0
- package/dist/scanner/python.js +57 -0
- package/dist/scanner/python_parser.py +230 -0
- package/dist/scanner/rust.d.ts +23 -0
- package/dist/scanner/rust.js +304 -0
- package/dist/scanner/scanner.test.d.ts +1 -0
- package/dist/scanner/scanner.test.js +210 -0
- package/dist/scanner/types.d.ts +50 -0
- package/dist/scanner/types.js +1 -0
- package/dist/scanner/typescript.d.ts +34 -0
- package/dist/scanner/typescript.js +327 -0
- package/dist/scanner/typescript.manual-test.d.ts +1 -0
- package/dist/scanner/typescript.manual-test.js +112 -0
- package/dist/template/docs.json +32 -0
- package/dist/template/mdx-components.tsx +62 -0
- package/dist/template/next-env.d.ts +6 -0
- package/dist/template/next.config.mjs +17 -0
- package/dist/template/package.json +39 -0
- package/dist/template/postcss.config.mjs +5 -0
- package/dist/template/public/search-index.json +1 -0
- package/dist/template/scripts/build-search-index.mjs +120 -0
- package/dist/template/src/app/api/mock/[...path]/route.ts +224 -0
- package/dist/template/src/app/api/openapi/route.ts +48 -0
- package/dist/template/src/app/api/rate-limit/route.ts +84 -0
- package/dist/template/src/app/docs/[...slug]/page.tsx +81 -0
- package/dist/template/src/app/docs/layout.tsx +9 -0
- package/dist/template/src/app/docs/page.mdx +67 -0
- package/dist/template/src/app/error.tsx +63 -0
- package/dist/template/src/app/layout.tsx +71 -0
- package/dist/template/src/app/page.tsx +18 -0
- package/dist/template/src/app/reference/route.ts +36 -0
- package/dist/template/src/app/robots.ts +14 -0
- package/dist/template/src/app/sitemap.ts +64 -0
- package/dist/template/src/components/breadcrumbs.tsx +41 -0
- package/dist/template/src/components/copy-button.tsx +29 -0
- package/dist/template/src/components/docs-layout.tsx +35 -0
- package/dist/template/src/components/edit-link.tsx +39 -0
- package/dist/template/src/components/feedback.tsx +52 -0
- package/dist/template/src/components/header.tsx +66 -0
- package/dist/template/src/components/mdx/accordion.tsx +48 -0
- package/dist/template/src/components/mdx/api-badge.tsx +57 -0
- package/dist/template/src/components/mdx/callout.tsx +111 -0
- package/dist/template/src/components/mdx/card.tsx +62 -0
- package/dist/template/src/components/mdx/changelog.tsx +57 -0
- package/dist/template/src/components/mdx/code-block.tsx +42 -0
- package/dist/template/src/components/mdx/code-group.tsx +125 -0
- package/dist/template/src/components/mdx/code-playground.tsx +322 -0
- package/dist/template/src/components/mdx/go-playground.tsx +235 -0
- package/dist/template/src/components/mdx/heading.tsx +37 -0
- package/dist/template/src/components/mdx/highlighted-code.tsx +89 -0
- package/dist/template/src/components/mdx/index.tsx +15 -0
- package/dist/template/src/components/mdx/param-table.tsx +71 -0
- package/dist/template/src/components/mdx/python-playground.tsx +293 -0
- package/dist/template/src/components/mdx/steps.tsx +43 -0
- package/dist/template/src/components/mdx/tabs.tsx +81 -0
- package/dist/template/src/components/rate-limit-display.tsx +183 -0
- package/dist/template/src/components/search-dialog.tsx +178 -0
- package/dist/template/src/components/sidebar.tsx +129 -0
- package/dist/template/src/components/syntax-theme-selector.tsx +50 -0
- package/dist/template/src/components/table-of-contents.tsx +84 -0
- package/dist/template/src/components/theme-toggle.tsx +46 -0
- package/dist/template/src/components/version-selector.tsx +61 -0
- package/dist/template/src/contexts/syntax-theme.tsx +52 -0
- package/dist/template/src/lib/highlight.ts +83 -0
- package/dist/template/src/lib/search-types.ts +37 -0
- package/dist/template/src/lib/search.ts +125 -0
- package/dist/template/src/lib/utils.ts +6 -0
- package/dist/template/src/styles/globals.css +152 -0
- package/dist/template/tsconfig.json +25 -0
- package/dist/template/tsconfig.tsbuildinfo +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
interface Param {
|
|
4
|
+
name: string
|
|
5
|
+
type: string
|
|
6
|
+
required?: boolean
|
|
7
|
+
default?: string
|
|
8
|
+
description: ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ParamTableProps {
|
|
12
|
+
params: Param[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ParamTable({ params }: ParamTableProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="my-6 overflow-x-auto">
|
|
18
|
+
<table className="w-full text-[14px]">
|
|
19
|
+
<thead>
|
|
20
|
+
<tr className="border-b border-[var(--color-border)]">
|
|
21
|
+
<th className="py-2 pr-4 text-left font-semibold">Parameter</th>
|
|
22
|
+
<th className="py-2 pr-4 text-left font-semibold">Type</th>
|
|
23
|
+
<th className="py-2 text-left font-semibold">Description</th>
|
|
24
|
+
</tr>
|
|
25
|
+
</thead>
|
|
26
|
+
<tbody>
|
|
27
|
+
{params.map((param) => (
|
|
28
|
+
<tr key={param.name} className="border-b border-[var(--color-border)]">
|
|
29
|
+
<td className="py-3 pr-4 align-top">
|
|
30
|
+
<code className="text-[13px] font-semibold">{param.name}</code>
|
|
31
|
+
{param.required && (
|
|
32
|
+
<span className="ml-1.5 text-[11px] text-red-500 font-medium">required</span>
|
|
33
|
+
)}
|
|
34
|
+
</td>
|
|
35
|
+
<td className="py-3 pr-4 align-top">
|
|
36
|
+
<code className="text-[13px] text-[var(--color-text-secondary)]">{param.type}</code>
|
|
37
|
+
{param.default && (
|
|
38
|
+
<span className="block text-[12px] text-[var(--color-text-tertiary)]">
|
|
39
|
+
Default: <code>{param.default}</code>
|
|
40
|
+
</span>
|
|
41
|
+
)}
|
|
42
|
+
</td>
|
|
43
|
+
<td className="py-3 align-top text-[var(--color-text-secondary)]">
|
|
44
|
+
{param.description}
|
|
45
|
+
</td>
|
|
46
|
+
</tr>
|
|
47
|
+
))}
|
|
48
|
+
</tbody>
|
|
49
|
+
</table>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Response/Request schema display
|
|
55
|
+
interface SchemaProps {
|
|
56
|
+
type: 'request' | 'response'
|
|
57
|
+
schema: Record<string, any>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function Schema({ type, schema }: SchemaProps) {
|
|
61
|
+
return (
|
|
62
|
+
<div className="my-4 rounded-lg border border-[var(--color-border)] overflow-hidden">
|
|
63
|
+
<div className="px-4 py-2 bg-[var(--color-bg-secondary)] border-b border-[var(--color-border)]">
|
|
64
|
+
<span className="text-[13px] font-medium capitalize">{type} Schema</span>
|
|
65
|
+
</div>
|
|
66
|
+
<pre className="!m-0 !rounded-none p-4 text-[13px] overflow-x-auto">
|
|
67
|
+
<code>{JSON.stringify(schema, null, 2)}</code>
|
|
68
|
+
</pre>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useCallback, useEffect } from 'react'
|
|
4
|
+
import { Play, RotateCcw, Download, Loader2, Square } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
interface PythonPlaygroundProps {
|
|
7
|
+
code: string
|
|
8
|
+
filename?: string
|
|
9
|
+
packages?: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type OutputLine = {
|
|
13
|
+
type: 'stdout' | 'stderr' | 'result'
|
|
14
|
+
content: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function PythonPlayground({
|
|
18
|
+
code: initialCode,
|
|
19
|
+
filename = 'main.py',
|
|
20
|
+
packages = [],
|
|
21
|
+
}: PythonPlaygroundProps) {
|
|
22
|
+
const [code, setCode] = useState(initialCode)
|
|
23
|
+
const [output, setOutput] = useState<OutputLine[]>([])
|
|
24
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
25
|
+
const [isRunning, setIsRunning] = useState(false)
|
|
26
|
+
const [pyodideReady, setPyodideReady] = useState(false)
|
|
27
|
+
const pyodideRef = useRef<any>(null)
|
|
28
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
29
|
+
|
|
30
|
+
// Load Pyodide on mount
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const loadPyodide = async () => {
|
|
33
|
+
if (pyodideRef.current) return
|
|
34
|
+
|
|
35
|
+
setIsLoading(true)
|
|
36
|
+
try {
|
|
37
|
+
// @ts-ignore - Pyodide loaded via script
|
|
38
|
+
const pyodide = await window.loadPyodide({
|
|
39
|
+
indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.24.1/full/',
|
|
40
|
+
})
|
|
41
|
+
pyodideRef.current = pyodide
|
|
42
|
+
|
|
43
|
+
// Install micropip for package management
|
|
44
|
+
await pyodide.loadPackage('micropip')
|
|
45
|
+
|
|
46
|
+
// Install requested packages
|
|
47
|
+
if (packages.length > 0) {
|
|
48
|
+
const micropip = pyodide.pyimport('micropip')
|
|
49
|
+
for (const pkg of packages) {
|
|
50
|
+
try {
|
|
51
|
+
await micropip.install(pkg)
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.warn(`Failed to install ${pkg}:`, e)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setPyodideReady(true)
|
|
59
|
+
} catch (err) {
|
|
60
|
+
setOutput([{ type: 'stderr', content: `Failed to load Python: ${err}` }])
|
|
61
|
+
} finally {
|
|
62
|
+
setIsLoading(false)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check if Pyodide script is loaded
|
|
67
|
+
if (typeof window !== 'undefined') {
|
|
68
|
+
if ((window as any).loadPyodide) {
|
|
69
|
+
loadPyodide()
|
|
70
|
+
} else {
|
|
71
|
+
// Load Pyodide script
|
|
72
|
+
const script = document.createElement('script')
|
|
73
|
+
script.src = 'https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js'
|
|
74
|
+
script.onload = loadPyodide
|
|
75
|
+
script.onerror = () => {
|
|
76
|
+
setIsLoading(false)
|
|
77
|
+
setOutput([{ type: 'stderr', content: 'Failed to load Python runtime. Check your network connection.' }])
|
|
78
|
+
}
|
|
79
|
+
document.head.appendChild(script)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}, [packages])
|
|
83
|
+
|
|
84
|
+
const runCode = useCallback(async () => {
|
|
85
|
+
if (!pyodideRef.current || isRunning) return
|
|
86
|
+
|
|
87
|
+
setIsRunning(true)
|
|
88
|
+
setOutput([])
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const pyodide = pyodideRef.current
|
|
92
|
+
|
|
93
|
+
// Capture stdout/stderr
|
|
94
|
+
pyodide.runPython(`
|
|
95
|
+
import sys
|
|
96
|
+
from io import StringIO
|
|
97
|
+
|
|
98
|
+
class CaptureOutput:
|
|
99
|
+
def __init__(self):
|
|
100
|
+
self.stdout = StringIO()
|
|
101
|
+
self.stderr = StringIO()
|
|
102
|
+
|
|
103
|
+
def get_output(self):
|
|
104
|
+
return self.stdout.getvalue(), self.stderr.getvalue()
|
|
105
|
+
|
|
106
|
+
def clear(self):
|
|
107
|
+
self.stdout = StringIO()
|
|
108
|
+
self.stderr = StringIO()
|
|
109
|
+
|
|
110
|
+
_capture = CaptureOutput()
|
|
111
|
+
sys.stdout = _capture.stdout
|
|
112
|
+
sys.stderr = _capture.stderr
|
|
113
|
+
`)
|
|
114
|
+
|
|
115
|
+
// Run the user's code
|
|
116
|
+
let result
|
|
117
|
+
try {
|
|
118
|
+
result = pyodide.runPython(code)
|
|
119
|
+
} catch (err: unknown) {
|
|
120
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
121
|
+
setOutput([{ type: 'stderr', content: message }])
|
|
122
|
+
setIsRunning(false)
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Get captured output
|
|
127
|
+
const [stdout, stderr] = pyodide.runPython('_capture.get_output()').toJs()
|
|
128
|
+
|
|
129
|
+
const lines: OutputLine[] = []
|
|
130
|
+
|
|
131
|
+
if (stdout) {
|
|
132
|
+
lines.push({ type: 'stdout', content: stdout })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (stderr) {
|
|
136
|
+
lines.push({ type: 'stderr', content: stderr })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (result !== undefined && result !== null) {
|
|
140
|
+
const resultStr = typeof result === 'object' && result.toJs
|
|
141
|
+
? JSON.stringify(result.toJs(), null, 2)
|
|
142
|
+
: String(result)
|
|
143
|
+
if (resultStr && resultStr !== 'undefined') {
|
|
144
|
+
lines.push({ type: 'result', content: resultStr })
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Reset stdout/stderr
|
|
149
|
+
pyodide.runPython('_capture.clear()')
|
|
150
|
+
|
|
151
|
+
setOutput(lines.length > 0 ? lines : [{ type: 'stdout', content: '(no output)' }])
|
|
152
|
+
} catch (err: unknown) {
|
|
153
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
154
|
+
setOutput([{ type: 'stderr', content: message }])
|
|
155
|
+
} finally {
|
|
156
|
+
setIsRunning(false)
|
|
157
|
+
}
|
|
158
|
+
}, [code, isRunning])
|
|
159
|
+
|
|
160
|
+
const handleReset = () => {
|
|
161
|
+
setCode(initialCode)
|
|
162
|
+
setOutput([])
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const handleDownload = () => {
|
|
166
|
+
const blob = new Blob([code], { type: 'text/plain' })
|
|
167
|
+
const url = URL.createObjectURL(blob)
|
|
168
|
+
const a = document.createElement('a')
|
|
169
|
+
a.href = url
|
|
170
|
+
a.download = filename
|
|
171
|
+
a.click()
|
|
172
|
+
URL.revokeObjectURL(url)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
176
|
+
// Run on Ctrl/Cmd + Enter
|
|
177
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
178
|
+
e.preventDefault()
|
|
179
|
+
runCode()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Tab handling
|
|
183
|
+
if (e.key === 'Tab') {
|
|
184
|
+
e.preventDefault()
|
|
185
|
+
const start = textareaRef.current?.selectionStart || 0
|
|
186
|
+
const end = textareaRef.current?.selectionEnd || 0
|
|
187
|
+
const newCode = code.slice(0, start) + ' ' + code.slice(end)
|
|
188
|
+
setCode(newCode)
|
|
189
|
+
// Reset cursor position
|
|
190
|
+
setTimeout(() => {
|
|
191
|
+
if (textareaRef.current) {
|
|
192
|
+
textareaRef.current.selectionStart = textareaRef.current.selectionEnd = start + 4
|
|
193
|
+
}
|
|
194
|
+
}, 0)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div className="my-6 rounded-lg border border-[var(--color-border)] overflow-hidden">
|
|
200
|
+
{/* Toolbar */}
|
|
201
|
+
<div className="flex items-center justify-between px-3 py-2 bg-[var(--color-bg-secondary)] border-b border-[var(--color-border)]">
|
|
202
|
+
<div className="flex items-center gap-2">
|
|
203
|
+
<span className="text-xs font-medium text-[var(--color-text-secondary)]">
|
|
204
|
+
🐍 Python
|
|
205
|
+
</span>
|
|
206
|
+
<span className="text-xs text-[var(--color-text-tertiary)]">
|
|
207
|
+
{filename}
|
|
208
|
+
</span>
|
|
209
|
+
</div>
|
|
210
|
+
<div className="flex items-center gap-1">
|
|
211
|
+
<button
|
|
212
|
+
onClick={handleReset}
|
|
213
|
+
className="p-1.5 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] rounded"
|
|
214
|
+
title="Reset code"
|
|
215
|
+
>
|
|
216
|
+
<RotateCcw size={14} />
|
|
217
|
+
</button>
|
|
218
|
+
<button
|
|
219
|
+
onClick={handleDownload}
|
|
220
|
+
className="p-1.5 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] rounded"
|
|
221
|
+
title="Download code"
|
|
222
|
+
>
|
|
223
|
+
<Download size={14} />
|
|
224
|
+
</button>
|
|
225
|
+
<button
|
|
226
|
+
onClick={runCode}
|
|
227
|
+
disabled={!pyodideReady || isRunning}
|
|
228
|
+
className="flex items-center gap-1 px-2 py-1 text-xs font-medium bg-emerald-600 text-white rounded hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
229
|
+
title="Run (Ctrl+Enter)"
|
|
230
|
+
>
|
|
231
|
+
{isLoading ? (
|
|
232
|
+
<>
|
|
233
|
+
<Loader2 size={12} className="animate-spin" />
|
|
234
|
+
Loading...
|
|
235
|
+
</>
|
|
236
|
+
) : isRunning ? (
|
|
237
|
+
<>
|
|
238
|
+
<Square size={12} />
|
|
239
|
+
Running
|
|
240
|
+
</>
|
|
241
|
+
) : (
|
|
242
|
+
<>
|
|
243
|
+
<Play size={12} />
|
|
244
|
+
Run
|
|
245
|
+
</>
|
|
246
|
+
)}
|
|
247
|
+
</button>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
{/* Code editor */}
|
|
252
|
+
<div className="relative">
|
|
253
|
+
<textarea
|
|
254
|
+
ref={textareaRef}
|
|
255
|
+
value={code}
|
|
256
|
+
onChange={(e) => setCode(e.target.value)}
|
|
257
|
+
onKeyDown={handleKeyDown}
|
|
258
|
+
spellCheck={false}
|
|
259
|
+
className="w-full min-h-[200px] p-4 font-mono text-sm bg-[var(--color-bg)] text-[var(--color-text)] resize-none focus:outline-none"
|
|
260
|
+
style={{
|
|
261
|
+
tabSize: 4,
|
|
262
|
+
}}
|
|
263
|
+
/>
|
|
264
|
+
<div className="absolute top-2 right-2 text-xs text-[var(--color-text-tertiary)]">
|
|
265
|
+
Ctrl+Enter to run
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{/* Output */}
|
|
270
|
+
{output.length > 0 && (
|
|
271
|
+
<div className="border-t border-[var(--color-border)] bg-[var(--color-code-bg)] text-[var(--color-code-text)] p-4 font-mono text-sm max-h-[300px] overflow-auto">
|
|
272
|
+
<div className="text-xs text-[var(--color-text-tertiary)] mb-2">Output:</div>
|
|
273
|
+
{output.map((line, i) => (
|
|
274
|
+
<pre
|
|
275
|
+
key={i}
|
|
276
|
+
className={`whitespace-pre-wrap ${
|
|
277
|
+
line.type === 'stderr'
|
|
278
|
+
? 'text-red-400'
|
|
279
|
+
: line.type === 'result'
|
|
280
|
+
? 'text-emerald-400'
|
|
281
|
+
: ''
|
|
282
|
+
}`}
|
|
283
|
+
>
|
|
284
|
+
{line.content}
|
|
285
|
+
</pre>
|
|
286
|
+
))}
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export default PythonPlayground
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ReactNode, Children, isValidElement } from 'react'
|
|
2
|
+
|
|
3
|
+
interface StepsProps {
|
|
4
|
+
children: ReactNode
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Steps({ children }: StepsProps) {
|
|
8
|
+
const items = Children.toArray(children).filter(isValidElement)
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="my-6 space-y-6">
|
|
12
|
+
{items.map((child, index) => (
|
|
13
|
+
<div key={`step-${index}`} className="flex gap-4">
|
|
14
|
+
<div className="flex flex-col items-center">
|
|
15
|
+
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-[var(--color-primary)] text-white text-sm font-medium">
|
|
16
|
+
{index + 1}
|
|
17
|
+
</div>
|
|
18
|
+
{index < items.length - 1 && (
|
|
19
|
+
<div className="flex-1 w-px bg-[var(--color-border)] mt-2" />
|
|
20
|
+
)}
|
|
21
|
+
</div>
|
|
22
|
+
<div className="flex-1 pb-6">
|
|
23
|
+
{child}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface StepProps {
|
|
32
|
+
title: string
|
|
33
|
+
children: ReactNode
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function Step({ title, children }: StepProps) {
|
|
37
|
+
return (
|
|
38
|
+
<div>
|
|
39
|
+
<h3 className="font-medium text-[var(--color-text)] mb-2">{title}</h3>
|
|
40
|
+
<div className="text-[var(--color-text-secondary)]">{children}</div>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useState, ReactNode } from 'react'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
|
|
6
|
+
interface TabsContextValue {
|
|
7
|
+
activeTab: string
|
|
8
|
+
setActiveTab: (tab: string) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const TabsContext = createContext<TabsContextValue | null>(null)
|
|
12
|
+
|
|
13
|
+
interface TabsProps {
|
|
14
|
+
defaultValue: string
|
|
15
|
+
children: ReactNode
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Tabs({ defaultValue, children }: TabsProps) {
|
|
19
|
+
const [activeTab, setActiveTab] = useState(defaultValue)
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
|
|
23
|
+
<div className="my-6">{children}</div>
|
|
24
|
+
</TabsContext.Provider>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface TabListProps {
|
|
29
|
+
children: ReactNode
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function TabList({ children }: TabListProps) {
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex border-b border-[var(--color-border)]">
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface TabProps {
|
|
41
|
+
value: string
|
|
42
|
+
children: ReactNode
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function Tab({ value, children }: TabProps) {
|
|
46
|
+
const context = useContext(TabsContext)
|
|
47
|
+
if (!context) throw new Error('Tab must be used within Tabs')
|
|
48
|
+
|
|
49
|
+
const { activeTab, setActiveTab } = context
|
|
50
|
+
const isActive = activeTab === value
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<button
|
|
54
|
+
onClick={() => setActiveTab(value)}
|
|
55
|
+
className={cn(
|
|
56
|
+
'px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px',
|
|
57
|
+
isActive
|
|
58
|
+
? 'border-[var(--color-primary)] text-[var(--color-primary)]'
|
|
59
|
+
: 'border-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text)]'
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
{children}
|
|
63
|
+
</button>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface TabPanelProps {
|
|
68
|
+
value: string
|
|
69
|
+
children: ReactNode
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function TabPanel({ value, children }: TabPanelProps) {
|
|
73
|
+
const context = useContext(TabsContext)
|
|
74
|
+
if (!context) throw new Error('TabPanel must be used within Tabs')
|
|
75
|
+
|
|
76
|
+
const { activeTab } = context
|
|
77
|
+
|
|
78
|
+
if (activeTab !== value) return null
|
|
79
|
+
|
|
80
|
+
return <div className="pt-4">{children}</div>
|
|
81
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react'
|
|
4
|
+
import { Activity, AlertTriangle, CheckCircle } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
interface RateLimitInfo {
|
|
7
|
+
limit: number
|
|
8
|
+
remaining: number
|
|
9
|
+
reset: Date
|
|
10
|
+
used: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface RateLimitDisplayProps {
|
|
14
|
+
/** API endpoint to check rate limits for */
|
|
15
|
+
endpoint?: string
|
|
16
|
+
/** API key header name */
|
|
17
|
+
apiKeyHeader?: string
|
|
18
|
+
/** Refresh interval in ms (default: 30000) */
|
|
19
|
+
refreshInterval?: number
|
|
20
|
+
/** Show compact view */
|
|
21
|
+
compact?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Display current API rate limit status
|
|
26
|
+
* Parses standard rate limit headers (X-RateLimit-*, RateLimit-*)
|
|
27
|
+
*/
|
|
28
|
+
export function RateLimitDisplay({
|
|
29
|
+
endpoint = '/api/rate-limit',
|
|
30
|
+
apiKeyHeader = 'Authorization',
|
|
31
|
+
refreshInterval = 30000,
|
|
32
|
+
compact = false,
|
|
33
|
+
}: RateLimitDisplayProps) {
|
|
34
|
+
const [rateLimit, setRateLimit] = useState<RateLimitInfo | null>(null)
|
|
35
|
+
const [error, setError] = useState<string | null>(null)
|
|
36
|
+
const [loading, setLoading] = useState(true)
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const fetchRateLimit = async () => {
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(endpoint, {
|
|
42
|
+
method: 'HEAD',
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Parse rate limit headers
|
|
46
|
+
const limit = parseInt(
|
|
47
|
+
response.headers.get('X-RateLimit-Limit') ||
|
|
48
|
+
response.headers.get('RateLimit-Limit') ||
|
|
49
|
+
response.headers.get('X-Rate-Limit-Limit') ||
|
|
50
|
+
'1000'
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const remaining = parseInt(
|
|
54
|
+
response.headers.get('X-RateLimit-Remaining') ||
|
|
55
|
+
response.headers.get('RateLimit-Remaining') ||
|
|
56
|
+
response.headers.get('X-Rate-Limit-Remaining') ||
|
|
57
|
+
'1000'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const resetTimestamp = parseInt(
|
|
61
|
+
response.headers.get('X-RateLimit-Reset') ||
|
|
62
|
+
response.headers.get('RateLimit-Reset') ||
|
|
63
|
+
response.headers.get('X-Rate-Limit-Reset') ||
|
|
64
|
+
String(Math.floor(Date.now() / 1000) + 3600)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
setRateLimit({
|
|
68
|
+
limit,
|
|
69
|
+
remaining,
|
|
70
|
+
reset: new Date(resetTimestamp * 1000),
|
|
71
|
+
used: limit - remaining,
|
|
72
|
+
})
|
|
73
|
+
setError(null)
|
|
74
|
+
} catch (err) {
|
|
75
|
+
setError('Unable to fetch rate limit info')
|
|
76
|
+
} finally {
|
|
77
|
+
setLoading(false)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fetchRateLimit()
|
|
82
|
+
const interval = setInterval(fetchRateLimit, refreshInterval)
|
|
83
|
+
return () => clearInterval(interval)
|
|
84
|
+
}, [endpoint, refreshInterval])
|
|
85
|
+
|
|
86
|
+
if (loading) {
|
|
87
|
+
return (
|
|
88
|
+
<div className="flex items-center gap-2 text-sm text-[var(--color-text-tertiary)]">
|
|
89
|
+
<Activity size={14} className="animate-pulse" />
|
|
90
|
+
<span>Loading rate limits...</span>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (error || !rateLimit) {
|
|
96
|
+
return (
|
|
97
|
+
<div className="flex items-center gap-2 text-sm text-yellow-600 dark:text-yellow-400">
|
|
98
|
+
<AlertTriangle size={14} />
|
|
99
|
+
<span>{error || 'Rate limit info unavailable'}</span>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const percentUsed = (rateLimit.used / rateLimit.limit) * 100
|
|
105
|
+
const isWarning = percentUsed > 80
|
|
106
|
+
const isCritical = percentUsed > 95
|
|
107
|
+
|
|
108
|
+
const statusColor = isCritical
|
|
109
|
+
? 'text-red-600 dark:text-red-400'
|
|
110
|
+
: isWarning
|
|
111
|
+
? 'text-yellow-600 dark:text-yellow-400'
|
|
112
|
+
: 'text-emerald-600 dark:text-emerald-400'
|
|
113
|
+
|
|
114
|
+
const barColor = isCritical
|
|
115
|
+
? 'bg-red-500'
|
|
116
|
+
: isWarning
|
|
117
|
+
? 'bg-yellow-500'
|
|
118
|
+
: 'bg-emerald-500'
|
|
119
|
+
|
|
120
|
+
const timeUntilReset = Math.max(0, Math.floor((rateLimit.reset.getTime() - Date.now()) / 1000 / 60))
|
|
121
|
+
|
|
122
|
+
if (compact) {
|
|
123
|
+
return (
|
|
124
|
+
<div className="flex items-center gap-2 text-sm">
|
|
125
|
+
<div className={statusColor}>
|
|
126
|
+
{isCritical ? <AlertTriangle size={14} /> : <CheckCircle size={14} />}
|
|
127
|
+
</div>
|
|
128
|
+
<span className={statusColor}>
|
|
129
|
+
{rateLimit.remaining}/{rateLimit.limit}
|
|
130
|
+
</span>
|
|
131
|
+
<span className="text-[var(--color-text-tertiary)]">
|
|
132
|
+
(resets in {timeUntilReset}m)
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className="p-4 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-secondary)]">
|
|
140
|
+
<div className="flex items-center justify-between mb-3">
|
|
141
|
+
<div className="flex items-center gap-2">
|
|
142
|
+
<Activity size={16} className={statusColor} />
|
|
143
|
+
<span className="font-medium text-[var(--color-text)]">Rate Limit Status</span>
|
|
144
|
+
</div>
|
|
145
|
+
<div className={`text-sm font-medium ${statusColor}`}>
|
|
146
|
+
{rateLimit.remaining} remaining
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{/* Progress bar */}
|
|
151
|
+
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden mb-3">
|
|
152
|
+
<div
|
|
153
|
+
className={`h-full transition-all ${barColor}`}
|
|
154
|
+
style={{ width: `${Math.min(100, percentUsed)}%` }}
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div className="grid grid-cols-3 gap-4 text-sm">
|
|
159
|
+
<div>
|
|
160
|
+
<div className="text-[var(--color-text-tertiary)]">Used</div>
|
|
161
|
+
<div className="font-medium text-[var(--color-text)]">{rateLimit.used}</div>
|
|
162
|
+
</div>
|
|
163
|
+
<div>
|
|
164
|
+
<div className="text-[var(--color-text-tertiary)]">Limit</div>
|
|
165
|
+
<div className="font-medium text-[var(--color-text)]">{rateLimit.limit}</div>
|
|
166
|
+
</div>
|
|
167
|
+
<div>
|
|
168
|
+
<div className="text-[var(--color-text-tertiary)]">Resets in</div>
|
|
169
|
+
<div className="font-medium text-[var(--color-text)]">{timeUntilReset} min</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{isCritical && (
|
|
174
|
+
<div className="mt-3 p-2 bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 text-sm rounded">
|
|
175
|
+
<AlertTriangle size={14} className="inline mr-1" />
|
|
176
|
+
Rate limit nearly exhausted. Requests may be throttled.
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export default RateLimitDisplay
|