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.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/autofix/index.d.ts +46 -0
  4. package/dist/autofix/index.js +240 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +40 -0
  7. package/dist/commands/autofix.d.ts +2 -0
  8. package/dist/commands/autofix.js +143 -0
  9. package/dist/commands/generate.d.ts +2 -0
  10. package/dist/commands/generate.js +320 -0
  11. package/dist/commands/init.d.ts +2 -0
  12. package/dist/commands/init.js +56 -0
  13. package/dist/commands/review-pr.d.ts +2 -0
  14. package/dist/commands/review-pr.js +117 -0
  15. package/dist/commands/watch.d.ts +2 -0
  16. package/dist/commands/watch.js +142 -0
  17. package/dist/config/index.d.ts +2 -0
  18. package/dist/config/index.js +2 -0
  19. package/dist/config/loader.d.ts +9 -0
  20. package/dist/config/loader.js +82 -0
  21. package/dist/config/types.d.ts +24 -0
  22. package/dist/config/types.js +34 -0
  23. package/dist/generator/generator.d.ts +15 -0
  24. package/dist/generator/generator.js +144 -0
  25. package/dist/generator/index.d.ts +4 -0
  26. package/dist/generator/index.js +4 -0
  27. package/dist/generator/organizer.d.ts +29 -0
  28. package/dist/generator/organizer.js +222 -0
  29. package/dist/generator/types.d.ts +83 -0
  30. package/dist/generator/types.js +1 -0
  31. package/dist/generator/writer.d.ts +28 -0
  32. package/dist/generator/writer.js +320 -0
  33. package/dist/github/pr-comments.d.ts +40 -0
  34. package/dist/github/pr-comments.js +308 -0
  35. package/dist/llm/anthropic-client.d.ts +16 -0
  36. package/dist/llm/anthropic-client.js +92 -0
  37. package/dist/llm/index.d.ts +53 -0
  38. package/dist/llm/index.js +400 -0
  39. package/dist/llm/llm.manual-test.d.ts +1 -0
  40. package/dist/llm/llm.manual-test.js +112 -0
  41. package/dist/llm/llm.mock-test.d.ts +4 -0
  42. package/dist/llm/llm.mock-test.js +79 -0
  43. package/dist/llm/openai-client.d.ts +17 -0
  44. package/dist/llm/openai-client.js +90 -0
  45. package/dist/llm/types.d.ts +60 -0
  46. package/dist/llm/types.js +20 -0
  47. package/dist/scanner/content-type.d.ts +39 -0
  48. package/dist/scanner/content-type.js +194 -0
  49. package/dist/scanner/content-type.test.d.ts +1 -0
  50. package/dist/scanner/content-type.test.js +231 -0
  51. package/dist/scanner/go.d.ts +20 -0
  52. package/dist/scanner/go.js +269 -0
  53. package/dist/scanner/index.d.ts +21 -0
  54. package/dist/scanner/index.js +137 -0
  55. package/dist/scanner/python.d.ts +6 -0
  56. package/dist/scanner/python.js +57 -0
  57. package/dist/scanner/python_parser.py +230 -0
  58. package/dist/scanner/rust.d.ts +23 -0
  59. package/dist/scanner/rust.js +304 -0
  60. package/dist/scanner/scanner.test.d.ts +1 -0
  61. package/dist/scanner/scanner.test.js +210 -0
  62. package/dist/scanner/types.d.ts +50 -0
  63. package/dist/scanner/types.js +1 -0
  64. package/dist/scanner/typescript.d.ts +34 -0
  65. package/dist/scanner/typescript.js +327 -0
  66. package/dist/scanner/typescript.manual-test.d.ts +1 -0
  67. package/dist/scanner/typescript.manual-test.js +112 -0
  68. package/dist/template/docs.json +32 -0
  69. package/dist/template/mdx-components.tsx +62 -0
  70. package/dist/template/next-env.d.ts +6 -0
  71. package/dist/template/next.config.mjs +17 -0
  72. package/dist/template/package.json +39 -0
  73. package/dist/template/postcss.config.mjs +5 -0
  74. package/dist/template/public/search-index.json +1 -0
  75. package/dist/template/scripts/build-search-index.mjs +120 -0
  76. package/dist/template/src/app/api/mock/[...path]/route.ts +224 -0
  77. package/dist/template/src/app/api/openapi/route.ts +48 -0
  78. package/dist/template/src/app/api/rate-limit/route.ts +84 -0
  79. package/dist/template/src/app/docs/[...slug]/page.tsx +81 -0
  80. package/dist/template/src/app/docs/layout.tsx +9 -0
  81. package/dist/template/src/app/docs/page.mdx +67 -0
  82. package/dist/template/src/app/error.tsx +63 -0
  83. package/dist/template/src/app/layout.tsx +71 -0
  84. package/dist/template/src/app/page.tsx +18 -0
  85. package/dist/template/src/app/reference/route.ts +36 -0
  86. package/dist/template/src/app/robots.ts +14 -0
  87. package/dist/template/src/app/sitemap.ts +64 -0
  88. package/dist/template/src/components/breadcrumbs.tsx +41 -0
  89. package/dist/template/src/components/copy-button.tsx +29 -0
  90. package/dist/template/src/components/docs-layout.tsx +35 -0
  91. package/dist/template/src/components/edit-link.tsx +39 -0
  92. package/dist/template/src/components/feedback.tsx +52 -0
  93. package/dist/template/src/components/header.tsx +66 -0
  94. package/dist/template/src/components/mdx/accordion.tsx +48 -0
  95. package/dist/template/src/components/mdx/api-badge.tsx +57 -0
  96. package/dist/template/src/components/mdx/callout.tsx +111 -0
  97. package/dist/template/src/components/mdx/card.tsx +62 -0
  98. package/dist/template/src/components/mdx/changelog.tsx +57 -0
  99. package/dist/template/src/components/mdx/code-block.tsx +42 -0
  100. package/dist/template/src/components/mdx/code-group.tsx +125 -0
  101. package/dist/template/src/components/mdx/code-playground.tsx +322 -0
  102. package/dist/template/src/components/mdx/go-playground.tsx +235 -0
  103. package/dist/template/src/components/mdx/heading.tsx +37 -0
  104. package/dist/template/src/components/mdx/highlighted-code.tsx +89 -0
  105. package/dist/template/src/components/mdx/index.tsx +15 -0
  106. package/dist/template/src/components/mdx/param-table.tsx +71 -0
  107. package/dist/template/src/components/mdx/python-playground.tsx +293 -0
  108. package/dist/template/src/components/mdx/steps.tsx +43 -0
  109. package/dist/template/src/components/mdx/tabs.tsx +81 -0
  110. package/dist/template/src/components/rate-limit-display.tsx +183 -0
  111. package/dist/template/src/components/search-dialog.tsx +178 -0
  112. package/dist/template/src/components/sidebar.tsx +129 -0
  113. package/dist/template/src/components/syntax-theme-selector.tsx +50 -0
  114. package/dist/template/src/components/table-of-contents.tsx +84 -0
  115. package/dist/template/src/components/theme-toggle.tsx +46 -0
  116. package/dist/template/src/components/version-selector.tsx +61 -0
  117. package/dist/template/src/contexts/syntax-theme.tsx +52 -0
  118. package/dist/template/src/lib/highlight.ts +83 -0
  119. package/dist/template/src/lib/search-types.ts +37 -0
  120. package/dist/template/src/lib/search.ts +125 -0
  121. package/dist/template/src/lib/utils.ts +6 -0
  122. package/dist/template/src/styles/globals.css +152 -0
  123. package/dist/template/tsconfig.json +25 -0
  124. package/dist/template/tsconfig.tsbuildinfo +1 -0
  125. 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