import { useState, useEffect } from 'react'; import { ChevronDown, ChevronRight, Play, Copy, Loader2, ExternalLink, AlertCircle, CheckCircle } from 'lucide-react'; import { Card, CardContent } from './Card'; import { Button } from './Button'; interface OpenAPISchema { paths: Record>; components?: { schemas?: Record; }; } interface EndpointSpec { summary?: string; description?: string; tags?: string[]; parameters?: ParameterSpec[]; requestBody?: { content?: { 'application/json'?: { schema?: SchemaSpec; }; }; }; responses?: Record; } interface ParameterSpec { name: string; in: 'path' | 'query' | 'header'; required?: boolean; description?: string; schema?: { type?: string; default?: unknown; enum?: string[]; }; } interface SchemaSpec { type?: string; properties?: Record; required?: string[]; items?: SchemaSpec; $ref?: string; allOf?: SchemaSpec[]; anyOf?: SchemaSpec[]; oneOf?: SchemaSpec[]; default?: unknown; description?: string; enum?: string[]; example?: unknown; } interface ResponseSpec { description?: string; content?: { 'application/json'?: { schema?: SchemaSpec; }; }; } interface APIResponse { status: number; statusText: string; headers: Record; body: unknown; duration: number; } const METHOD_COLORS: Record = { get: 'bg-blue-500/20 text-blue-400 border-blue-500/30', post: 'bg-green-500/20 text-green-400 border-green-500/30', put: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30', patch: 'bg-orange-500/20 text-orange-400 border-orange-500/30', delete: 'bg-red-500/20 text-red-400 border-red-500/30', }; function resolveRef(schema: OpenAPISchema, ref: string): SchemaSpec { // Parse $ref like "#/components/schemas/PrinterCreate" const parts = ref.replace('#/', '').split('/'); let current: unknown = schema; for (const part of parts) { current = (current as Record)[part]; } return current as SchemaSpec; } function getSchemaExample(schema: OpenAPISchema, spec: SchemaSpec, depth = 0): unknown { if (depth > 5) return '...'; if (spec.$ref) { return getSchemaExample(schema, resolveRef(schema, spec.$ref), depth + 1); } if (spec.allOf) { const merged: Record = {}; for (const sub of spec.allOf) { const subExample = getSchemaExample(schema, sub, depth + 1); if (typeof subExample === 'object' && subExample !== null) { Object.assign(merged, subExample); } } return merged; } if (spec.example !== undefined) return spec.example; if (spec.default !== undefined) return spec.default; switch (spec.type) { case 'string': if (spec.enum) return spec.enum[0]; return 'string'; case 'integer': case 'number': return 0; case 'boolean': return false; case 'array': return spec.items ? [getSchemaExample(schema, spec.items, depth + 1)] : []; case 'object': if (spec.properties) { const obj: Record = {}; for (const [key, propSpec] of Object.entries(spec.properties)) { obj[key] = getSchemaExample(schema, propSpec, depth + 1); } return obj; } return {}; default: return null; } } interface EndpointItemProps { path: string; method: string; spec: EndpointSpec; schema: OpenAPISchema; apiKey: string; } function EndpointItem({ path, method, spec, schema, apiKey }: EndpointItemProps) { const [expanded, setExpanded] = useState(false); const [params, setParams] = useState>({}); const [bodyText, setBodyText] = useState(''); const [response, setResponse] = useState(null); const [loading, setLoading] = useState(false); const [copied, setCopied] = useState(false); // Initialize params with defaults useEffect(() => { if (expanded && spec.parameters) { const defaults: Record = {}; for (const param of spec.parameters) { if (param.schema?.default !== undefined) { defaults[param.name] = String(param.schema.default); } } setParams(prev => ({ ...defaults, ...prev })); } }, [expanded, spec.parameters]); // Initialize body with example useEffect(() => { if (expanded && spec.requestBody?.content?.['application/json']?.schema && !bodyText) { const bodySchema = spec.requestBody.content['application/json'].schema; const example = getSchemaExample(schema, bodySchema); setBodyText(JSON.stringify(example, null, 2)); } }, [expanded, spec.requestBody, schema, bodyText]); // Check for missing required parameters const getMissingParams = () => { const missing: string[] = []; for (const param of spec.parameters || []) { if (param.in === 'path' || param.required) { const value = params[param.name]; if (value === undefined || value === '') { missing.push(param.name); } } } return missing; }; const missingParams = getMissingParams(); const executeRequest = async () => { if (missingParams.length > 0) { setResponse({ status: 0, statusText: 'Validation Error', headers: {}, body: `Missing required parameters: ${missingParams.join(', ')}`, duration: 0, }); return; } setLoading(true); setResponse(null); try { // Build URL with path and query params let url = path; const queryParams = new URLSearchParams(); for (const param of spec.parameters || []) { const value = params[param.name]; if (value !== undefined && value !== '') { if (param.in === 'path') { url = url.replace(`{${param.name}}`, encodeURIComponent(value)); } else if (param.in === 'query') { queryParams.append(param.name, value); } } } const queryString = queryParams.toString(); // OpenAPI paths already include /api/v1 prefix const fullUrl = `${url}${queryString ? `?${queryString}` : ''}`; const headers: Record = { 'Content-Type': 'application/json', }; if (apiKey) { headers['X-API-Key'] = apiKey; } const options: RequestInit = { method: method.toUpperCase(), headers, }; if (['post', 'put', 'patch'].includes(method) && bodyText) { options.body = bodyText; } const startTime = performance.now(); const res = await fetch(fullUrl, options); const duration = Math.round(performance.now() - startTime); const responseHeaders: Record = {}; res.headers.forEach((value, key) => { responseHeaders[key] = value; }); let body: unknown; const contentType = res.headers.get('content-type'); if (contentType?.includes('application/json')) { body = await res.json(); } else { body = await res.text(); } setResponse({ status: res.status, statusText: res.statusText, headers: responseHeaders, body, duration, }); } catch (err) { setResponse({ status: 0, statusText: 'Network Error', headers: {}, body: err instanceof Error ? err.message : 'Unknown error', duration: 0, }); } finally { setLoading(false); } }; const copyResponse = async () => { if (response) { const text = typeof response.body === 'string' ? response.body : JSON.stringify(response.body, null, 2); try { await navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { // Fallback for non-HTTPS const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); setCopied(true); setTimeout(() => setCopied(false), 2000); } } }; const pathParams = (spec.parameters || []).filter(p => p.in === 'path'); const queryParamsSpec = (spec.parameters || []).filter(p => p.in === 'query'); const hasBody = ['post', 'put', 'patch'].includes(method) && spec.requestBody; return (
{expanded && (
{spec.description && (

{spec.description}

)} {/* Path Parameters */} {pathParams.length > 0 && (

Path Parameters

{pathParams.map(param => (
setParams(p => ({ ...p, [param.name]: e.target.value }))} placeholder={param.description || param.schema?.type || 'value'} className="flex-1 px-2 py-1 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white text-sm font-mono focus:border-bambu-green focus:outline-none" />
))}
)} {/* Query Parameters */} {queryParamsSpec.length > 0 && (

Query Parameters

{queryParamsSpec.map(param => (
{param.schema?.enum ? ( ) : ( setParams(p => ({ ...p, [param.name]: e.target.value }))} placeholder={param.description || param.schema?.type || 'value'} className="flex-1 px-2 py-1 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white text-sm font-mono focus:border-bambu-green focus:outline-none" /> )}
))}
)} {/* Request Body */} {hasBody && (

Request Body