import { useState } from "react"; import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { PluginMetadata } from "@/client/hooks/usePlugin"; import { Play, ChevronDown, ChevronUp, Copy, Check, AlertTriangle, XCircle } from "lucide-react"; import { CodeBlock } from "@/components/CodeBlock"; import { FileUpload } from "@/components/FileUpload"; import { getApiUrl } from "@/lib/api-url"; interface PluginCardProps { plugin: PluginMetadata; } const methodColors: Record = { GET: "bg-green-500/20 text-green-400 border-green-500/50", POST: "bg-blue-500/20 text-blue-400 border-blue-500/50", PUT: "bg-yellow-500/20 text-yellow-400 border-yellow-500/50", DELETE: "bg-red-500/20 text-red-400 border-red-500/50", PATCH: "bg-purple-500/20 text-purple-400 border-purple-500/50", }; export function PluginCard({ plugin }: PluginCardProps) { const [paramValues, setParamValues] = useState>({}); const [fileParams, setFileParams] = useState>({}); const [urlParams, setUrlParams] = useState>({}); const [response, setResponse] = useState(null); const [responseHeaders, setResponseHeaders] = useState>({}); const [requestUrl, setRequestUrl] = useState(""); const [loading, setLoading] = useState(false); const [isExpanded, setIsExpanded] = useState(false); const [copiedUrl, setCopiedUrl] = useState(false); const [copiedRequestUrl, setCopiedRequestUrl] = useState(false); const isDisabled = plugin.disabled; const isDeprecated = plugin.deprecated; const handleParamChange = (paramName: string, value: string) => { setParamValues((prev) => ({ ...prev, [paramName]: value })); }; const handleFileChange = (paramName: string, file: File | null) => { setFileParams((prev) => ({ ...prev, [paramName]: file })); }; const handleUrlParamChange = (paramName: string, url: string) => { setUrlParams((prev) => ({ ...prev, [paramName]: url })); }; const handleExecute = async () => { if (isDisabled) { setResponse({ status: 503, statusText: "Service Unavailable", data: { success: false, message: "Plugin is disabled", reason: plugin.disabledReason || "This plugin has been disabled" } }); return; } setLoading(true); try { let url = "/api" + plugin.endpoint; let fullUrl = getApiUrl(plugin.endpoint); // Check if we have any file parameters const hasFiles = Object.values(fileParams).some(file => file !== null); const hasUrls = Object.values(urlParams).some(url => url !== ""); if (plugin.method === "GET" && plugin.parameters?.query) { const queryParams = new URLSearchParams(); plugin.parameters.query.forEach((param) => { if (param.type === "file" && param.acceptUrl) { const urlValue = urlParams[param.name]; if (urlValue) { queryParams.append(param.name, urlValue); } } else { const value = paramValues[param.name]; if (value) { queryParams.append(param.name, value); } } }); if (queryParams.toString()) { url += "?" + queryParams.toString(); fullUrl += "?" + queryParams.toString(); } } setRequestUrl(fullUrl); const fetchOptions: RequestInit = { method: plugin.method, }; if (hasFiles || (hasUrls && plugin.method !== "GET")) { const formData = new FormData(); Object.entries(fileParams).forEach(([name, file]) => { if (file) { formData.append(name, file); } }); Object.entries(urlParams).forEach(([name, url]) => { if (url && !fileParams[name]) { formData.append(name, url); } }); if (plugin.parameters?.body) { plugin.parameters.body.forEach((param) => { if (param.type !== "file") { const value = paramValues[param.name]; if (value) { formData.append(param.name, value); } } }); } fetchOptions.body = formData; } else if (["POST", "PUT", "PATCH"].includes(plugin.method) && plugin.parameters?.body) { const bodyData: Record = {}; plugin.parameters.body.forEach((param) => { const value = paramValues[param.name]; if (value) { bodyData[param.name] = value; } }); fetchOptions.body = JSON.stringify(bodyData); fetchOptions.headers = { "Content-Type": "application/json", }; } // Add XMLHttpRequest fallback for FormData to avoid HTTP/2 issues let res; let data; if (fetchOptions.body instanceof FormData) { // Use XMLHttpRequest for FormData to ensure compatibility const xhr = new XMLHttpRequest(); const xhrPromise = new Promise((resolve, reject) => { xhr.onload = () => { try { const responseData = JSON.parse(xhr.responseText); resolve({ status: xhr.status, statusText: xhr.statusText, headers: new Headers(), json: async () => responseData }); } catch (e) { reject(new Error('Failed to parse response')); } }; xhr.onerror = () => reject(new Error('Network request failed')); xhr.ontimeout = () => reject(new Error('Request timeout')); xhr.open(plugin.method, url, true); xhr.timeout = 60000; // 60 second timeout xhr.send(fetchOptions.body as FormData); }); res = await xhrPromise as any; data = await res.json(); } else { // Use fetch for non-FormData requests res = await fetch(url, fetchOptions); data = await res.json(); } const headers: Record = {}; // Get headers from Response object if (res.headers && typeof res.headers.forEach === 'function') { res.headers.forEach((value: string, key: string) => { headers[key] = value; }); } setResponseHeaders(headers); setResponse({ status: res.status, statusText: res.statusText, data, }); } catch (error) { setResponse({ status: 500, statusText: "Error", data: { error: error instanceof Error ? error.message : "Unknown error" }, }); setResponseHeaders({}); } finally { setLoading(false); } }; const copyApiUrl = () => { const fullUrl = getApiUrl(plugin.endpoint); navigator.clipboard.writeText(fullUrl); setCopiedUrl(true); setTimeout(() => setCopiedUrl(false), 2000); }; const copyRequestUrl = () => { navigator.clipboard.writeText(requestUrl); setCopiedRequestUrl(true); setTimeout(() => setCopiedRequestUrl(false), 2000); }; const renderParameterInput = (param: any) => { if (param.type === "file") { return ( handleFileChange(param.name, file)} onUrlChange={param.acceptUrl ? (url) => handleUrlParamChange(param.name, url) : undefined} disabled={isDisabled} /> ); } if (param.enum && Array.isArray(param.enum) && param.enum.length > 0) { return ( ); } return ( handleParamChange(param.name, e.target.value)} disabled={isDisabled} className="bg-black/50 border-white/10 text-white focus:border-purple-500 disabled:opacity-50" /> ); }; const hasQueryParams = plugin.parameters?.query && plugin.parameters.query.length > 0; const hasBodyParams = plugin.parameters?.body && plugin.parameters.body.length > 0; const hasPathParams = plugin.parameters?.path && plugin.parameters.path.length > 0; const hasAnyParams = hasQueryParams || hasBodyParams || hasPathParams; const generateCurlExample = () => { let curl = `curl -X ${plugin.method} "${getApiUrl(plugin.endpoint)}`; if (hasQueryParams) { const exampleParams = plugin.parameters!.query! .map((p) => { if (p.type === "file" && p.acceptUrl) { return `${p.name}=https://example.com/file.jpg`; } return `${p.name}=${p.example || 'value'}`; }) .join('&'); curl += `?${exampleParams}`; } curl += '"'; if (hasBodyParams) { const hasFileParams = plugin.parameters!.body!.some(p => p.type === "file"); if (hasFileParams) { curl += ' \\\n'; plugin.parameters!.body!.forEach((p) => { if (p.type === "file") { if (p.acceptUrl) { curl += ` -F "${p.name}=https://example.com/file.jpg" \\\n`; } else { curl += ` -F "${p.name}=@/path/to/file" \\\n`; } } else { curl += ` -F "${p.name}=${p.example || 'value'}" \\\n`; } }); curl = curl.slice(0, -3); // Remove last \\\n } else { curl += ' \\\n -H "Content-Type: application/json" \\\n -d \''; const bodyExample: Record = {}; plugin.parameters!.body!.forEach((p) => { bodyExample[p.name] = p.example || 'value'; }); curl += JSON.stringify(bodyExample, null, 2); curl += "'"; } } return curl; }; const generateNodeExample = () => { const hasFileParams = plugin.parameters?.body?.some(p => p.type === "file") || false; if (hasFileParams) { let code = 'const formData = new FormData();\n'; plugin.parameters!.body!.forEach((p) => { if (p.type === "file") { if (p.acceptUrl) { code += `formData.append("${p.name}", "https://example.com/file.jpg");\n`; } else { code += `// For file upload from input: \n`; code += `const fileInput = document.getElementById("fileInput");\n`; code += `formData.append("${p.name}", fileInput.files[0]);\n`; } } else { code += `formData.append("${p.name}", "${p.example || 'value'}");\n`; } }); code += `\nconst response = await fetch("${getApiUrl(plugin.endpoint)}", {\n`; code += ` method: "${plugin.method}",\n`; code += ` body: formData\n`; code += '});\n\nconst data = await response.json();\nconsole.log(data);'; return code; } let code = `const response = await fetch("${getApiUrl(plugin.endpoint)}`; if (hasQueryParams) { const exampleParams = plugin.parameters!.query! .map((p) => { if (p.type === "file" && p.acceptUrl) { return `${p.name}=https://example.com/file.jpg`; } return `${p.name}=${p.example || 'value'}`; }) .join('&'); code += `?${exampleParams}`; } code += '", {\n method: "' + plugin.method + '"'; if (hasBodyParams && !hasFileParams) { code += ',\n headers: {\n "Content-Type": "application/json"\n },\n body: JSON.stringify('; const bodyExample: Record = {}; plugin.parameters!.body!.forEach((p) => { bodyExample[p.name] = p.example || 'value'; }); code += JSON.stringify(bodyExample, null, 2); code += ')'; } code += '\n});\n\nconst data = await response.json();\nconsole.log(data);'; return code; }; return ( {/* Header */}
setIsExpanded(!isExpanded)} >
{plugin.method} {plugin.endpoint} {isDisabled && ( Disabled )} {isDeprecated && !isDisabled && ( Deprecated )}

{plugin.name}

{plugin.description || "No description provided"}

{isDisabled && plugin.disabledReason && (

Plugin Disabled

{plugin.disabledReason}

)} {isDeprecated && !isDisabled && plugin.deprecatedReason && (

Plugin Deprecated

{plugin.deprecatedReason}

)} {plugin.tags && plugin.tags.length > 0 && (
{plugin.tags.map((tag) => ( {tag} ))}
)}
API URL: {getApiUrl(plugin.endpoint)}
{/* Expandable Content */} {isExpanded && ( Documentation Try It Out {/* Documentation Tab */} {hasAnyParams && (

Parameters

{[...(plugin.parameters?.path || []), ...(plugin.parameters?.query || []), ...(plugin.parameters?.body || [])].map((param) => ( ))}
Name Type Required Description
{param.name} {param.type} {param.type === "file" && param.fileConstraints && (
{param.fileConstraints.maxSize && (
Max: {(param.fileConstraints.maxSize / 1024 / 1024).toFixed(1)}MB
)} {param.fileConstraints.acceptedTypes && (
Types: {param.fileConstraints.acceptedTypes.join(', ')}
)}
)} {param.enum && ( (options: {param.enum.join(', ')}) )}
{param.required ? "Yes" : "No"} {param.description} {param.type === "file" && param.acceptUrl && ( ✓ Can also accept URL )}
)} {/* Responses section */} {plugin.responses && Object.keys(plugin.responses).length > 0 && (

Responses

{Object.entries(plugin.responses).map(([status, response]) => (
= 200 && parseInt(status) < 300 ? "bg-green-500/10" : parseInt(status) >= 400 && parseInt(status) < 500 ? "bg-yellow-500/10" : "bg-red-500/10" }`}> = 200 && parseInt(status) < 300 ? "bg-green-500/20 text-green-400 border-green-500/50" : parseInt(status) >= 400 && parseInt(status) < 500 ? "bg-yellow-500/20 text-yellow-400 border-yellow-500/50" : "bg-red-500/20 text-red-400 border-red-500/50" } border font-bold`} > {status} {response.description}
                        {JSON.stringify(response.example, null, 2)}
                      
))}
)} {/* Code Examples */}

Code Example

cURL
Node.js (fetch)
{/* Try It Out Tab */} {hasAnyParams ? (
{[...(plugin.parameters?.query || []), ...(plugin.parameters?.body || [])].map((param) => (
{param.type !== "file" && ( <> {renderParameterInput(param)}

{param.description}

)} {param.type === "file" && renderParameterInput(param)}
))}
) : (

No parameters required

)} {/* Response Display */} {response && (
{isDeprecated && responseHeaders['x-plugin-deprecated'] && (

Deprecation Warning

{responseHeaders['x-deprecation-reason'] || 'This plugin is deprecated'}

)} {requestUrl && (
Request URL
{requestUrl}
)}
Response Status = 200 && response.status < 300 ? "bg-green-500/20 text-green-400" : "bg-red-500/20 text-red-400" }`}> {response.status} {response.statusText}
{Object.keys(responseHeaders).length > 0 && (
Response Headers
{Object.entries(responseHeaders).map(([key, value]) => (
{key}:{" "} {value}
))}
)}
Response Body
)}
)}
); }