503 lines
18 KiB
TypeScript
503 lines
18 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* Interactive API Widget - Production Version
|
|
*
|
|
* Minimized layout with clean design system:
|
|
* - No inline API key input (uses DocsApiKeyInput component)
|
|
* - Clean header with method badge, endpoint, and expand button
|
|
* - Full-width code snippet area
|
|
* - Compact footer with API key anchor link and Try It button
|
|
* - Expanded view opens full-screen modal with code + response side-by-side
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
interface InteractiveAPIWidgetProps {
|
|
endpoint: string;
|
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
description: string;
|
|
parameters?: Array<{
|
|
name: string;
|
|
type: string;
|
|
required: boolean;
|
|
description: string;
|
|
defaultValue?: string;
|
|
}>;
|
|
}
|
|
|
|
type Language = 'curl' | 'javascript' | 'python' | 'go' | 'js-sdk' | 'mcp';
|
|
|
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
|
const API_KEY_STORAGE = 'banatie_docs_api_key';
|
|
|
|
const getMethodBadgeStyle = (method: string): string => {
|
|
switch (method) {
|
|
case 'GET':
|
|
return 'bg-cyan-600/20 text-cyan-400 border border-cyan-500/40';
|
|
case 'POST':
|
|
return 'bg-green-600/20 text-green-400 border border-green-500/40';
|
|
case 'PUT':
|
|
return 'bg-amber-600/20 text-amber-400 border border-amber-500/40';
|
|
case 'DELETE':
|
|
return 'bg-red-600/20 text-red-400 border border-red-500/40';
|
|
default:
|
|
return 'bg-slate-600/20 text-slate-400 border border-slate-500/40';
|
|
}
|
|
};
|
|
|
|
export const InteractiveAPIWidget = ({
|
|
endpoint,
|
|
method,
|
|
description,
|
|
parameters = [],
|
|
}: InteractiveAPIWidgetProps) => {
|
|
const [language, setLanguage] = useState<Language>('curl');
|
|
const [apiKey, setApiKey] = useState('');
|
|
const [isExecuting, setIsExecuting] = useState(false);
|
|
const [response, setResponse] = useState<any>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
const [paramValues, setParamValues] = useState<Record<string, string>>({});
|
|
|
|
// Load API key from localStorage and listen for changes
|
|
useEffect(() => {
|
|
const loadApiKey = () => {
|
|
const stored = localStorage.getItem(API_KEY_STORAGE);
|
|
if (stored) setApiKey(stored);
|
|
};
|
|
|
|
loadApiKey();
|
|
|
|
// Listen for API key changes from DocsApiKeyInput
|
|
const handleApiKeyChange = (e: CustomEvent) => {
|
|
setApiKey(e.detail);
|
|
};
|
|
|
|
window.addEventListener('apiKeyChanged', handleApiKeyChange as EventListener);
|
|
|
|
// Initialize parameter defaults
|
|
const defaults: Record<string, string> = {
|
|
prompt: 'a futuristic city at sunset',
|
|
filename: 'demo',
|
|
aspectRatio: '16:9',
|
|
};
|
|
|
|
parameters.forEach((param) => {
|
|
if (param.defaultValue) {
|
|
defaults[param.name] = param.defaultValue;
|
|
}
|
|
});
|
|
|
|
setParamValues(defaults);
|
|
|
|
return () => {
|
|
window.removeEventListener('apiKeyChanged', handleApiKeyChange as EventListener);
|
|
};
|
|
}, [parameters]);
|
|
|
|
// Get language display name
|
|
const getLanguageLabel = (lang: Language): string => {
|
|
switch (lang) {
|
|
case 'curl':
|
|
return 'cURL';
|
|
case 'javascript':
|
|
return 'JavaScript';
|
|
case 'python':
|
|
return 'Python';
|
|
case 'go':
|
|
return 'Go';
|
|
case 'js-sdk':
|
|
return 'JS SDK';
|
|
case 'mcp':
|
|
return 'MCP';
|
|
}
|
|
};
|
|
|
|
// Generate code examples
|
|
const generateCode = (): string => {
|
|
const url = `${API_BASE_URL}${endpoint}`;
|
|
|
|
// Coming soon placeholders
|
|
if (language === 'js-sdk' || language === 'mcp') {
|
|
return `# Coming soon\n# ${getLanguageLabel(language)} integration is under development`;
|
|
}
|
|
|
|
switch (language) {
|
|
case 'curl':
|
|
return `curl -X ${method} "${url}" \\
|
|
-H "X-API-Key: ${apiKey || 'YOUR_API_KEY'}" \\
|
|
-H "Content-Type: application/json" \\
|
|
-d '{
|
|
"prompt": "a futuristic city at sunset",
|
|
"filename": "city",
|
|
"aspectRatio": "16:9"
|
|
}'`;
|
|
|
|
case 'javascript':
|
|
return `const response = await fetch('${url}', {
|
|
method: '${method}',
|
|
headers: {
|
|
'X-API-Key': '${apiKey || 'YOUR_API_KEY'}',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
prompt: 'a futuristic city at sunset',
|
|
filename: 'city',
|
|
aspectRatio: '16:9'
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
console.log(data);`;
|
|
|
|
case 'python':
|
|
return `import requests
|
|
|
|
url = '${url}'
|
|
headers = {
|
|
'X-API-Key': '${apiKey || 'YOUR_API_KEY'}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
data = {
|
|
'prompt': 'a futuristic city at sunset',
|
|
'filename': 'city',
|
|
'aspectRatio': '16:9'
|
|
}
|
|
|
|
response = requests.${method.toLowerCase()}(url, headers=headers, json=data)
|
|
print(response.json())`;
|
|
|
|
case 'go':
|
|
return `package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
)
|
|
|
|
func main() {
|
|
url := "${url}"
|
|
data := map[string]interface{}{
|
|
"prompt": "a futuristic city at sunset",
|
|
"filename": "city",
|
|
"aspectRatio": "16:9",
|
|
}
|
|
|
|
jsonData, _ := json.Marshal(data)
|
|
req, _ := http.NewRequest("${method}", url, bytes.NewBuffer(jsonData))
|
|
req.Header.Set("X-API-Key", "${apiKey || 'YOUR_API_KEY'}")
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{}
|
|
resp, _ := client.Do(req)
|
|
defer resp.Body.Close()
|
|
}`;
|
|
|
|
default:
|
|
return '';
|
|
}
|
|
};
|
|
|
|
// Execute API request
|
|
const executeRequest = async () => {
|
|
if (!apiKey) {
|
|
setError('Please enter your API key in the top-right corner');
|
|
return;
|
|
}
|
|
|
|
setIsExecuting(true);
|
|
setError(null);
|
|
setResponse(null);
|
|
|
|
try {
|
|
const body = {
|
|
prompt: paramValues.prompt || 'a futuristic city at sunset',
|
|
filename: paramValues.filename || 'demo',
|
|
aspectRatio: paramValues.aspectRatio || '16:9',
|
|
};
|
|
|
|
console.log('🔵 API Request:', {
|
|
url: `${API_BASE_URL}${endpoint}`,
|
|
method,
|
|
headers: { 'X-API-Key': apiKey.substring(0, 10) + '...' },
|
|
body,
|
|
});
|
|
|
|
const res = await fetch(`${API_BASE_URL}${endpoint}`, {
|
|
method,
|
|
headers: {
|
|
'X-API-Key': apiKey,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: method !== 'GET' ? JSON.stringify(body) : undefined,
|
|
});
|
|
|
|
console.log('🟢 Response Status:', res.status, res.statusText);
|
|
|
|
if (!res.ok) {
|
|
let errorMessage = `HTTP ${res.status}: ${res.statusText}`;
|
|
try {
|
|
const errorData = await res.json();
|
|
errorMessage = errorData.message || errorData.error || errorMessage;
|
|
} catch {
|
|
// Response is not JSON
|
|
}
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
const data = await res.json();
|
|
console.log('✅ Response Data:', data);
|
|
setResponse(data);
|
|
} catch (err) {
|
|
console.error('❌ API Error:', err);
|
|
const errorMsg = err instanceof Error ? err.message : 'Failed to execute request';
|
|
setError(`Request failed: ${errorMsg}`);
|
|
} finally {
|
|
setIsExecuting(false);
|
|
}
|
|
};
|
|
|
|
// Copy code to clipboard
|
|
const copyCode = () => {
|
|
navigator.clipboard.writeText(generateCode());
|
|
};
|
|
|
|
// Expand API key input in navigation
|
|
const expandApiKey = () => {
|
|
window.dispatchEvent(new CustomEvent('expandApiKeyInput'));
|
|
};
|
|
|
|
const isSuccess = response && response.success === true;
|
|
|
|
return (
|
|
<>
|
|
{/* Minimized Widget */}
|
|
<div className="my-8 bg-slate-900/50 backdrop-blur-sm border border-slate-700/50 rounded-2xl overflow-hidden">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between bg-slate-950/80 px-6 py-4 border-b border-slate-700/50">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`px-3 py-1 rounded text-xs font-bold ${getMethodBadgeStyle(method)}`}>
|
|
{method}
|
|
</div>
|
|
<code className="text-sm text-gray-400">{endpoint}</code>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsExpanded(true)}
|
|
className="flex items-center gap-2 px-4 py-2 bg-cyan-600/10 hover:bg-cyan-600/20 text-cyan-400 rounded-lg transition-all text-sm font-medium border border-cyan-500/30 hover:border-cyan-500/50"
|
|
>
|
|
<span>⛶</span>
|
|
<span>Expand</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Code Section - Full Width */}
|
|
<div className="p-6">
|
|
{/* Language Tabs */}
|
|
<div className="flex items-center gap-2 mb-4 flex-wrap">
|
|
{(['curl', 'javascript', 'python', 'go', 'js-sdk', 'mcp'] as Language[]).map((lang) => (
|
|
<button
|
|
key={lang}
|
|
onClick={() => setLanguage(lang)}
|
|
className={`px-3 py-1.5 text-xs rounded-lg font-medium transition-all border ${
|
|
language === lang
|
|
? 'bg-slate-700 text-white border-slate-600'
|
|
: 'bg-slate-800/50 text-gray-400 border-slate-700/50 hover:border-slate-600 hover:text-gray-300'
|
|
}`}
|
|
>
|
|
{getLanguageLabel(lang)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Code Display */}
|
|
<div className="border border-slate-700/50 rounded-xl overflow-hidden">
|
|
<div className="flex items-center justify-between bg-slate-950/80 px-4 py-3 border-b border-slate-700/50">
|
|
<span className="text-xs text-gray-400 font-medium">Code Example</span>
|
|
<button
|
|
onClick={copyCode}
|
|
className="text-xs text-gray-400 hover:text-white transition-colors font-medium"
|
|
>
|
|
Copy
|
|
</button>
|
|
</div>
|
|
<div className="p-4 bg-slate-950/50 max-h-80 overflow-auto">
|
|
<pre className="text-sm text-gray-300 leading-relaxed">
|
|
<code>{generateCode()}</code>
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="flex items-center justify-between px-6 pb-6">
|
|
<button
|
|
onClick={expandApiKey}
|
|
className="text-sm text-gray-400 hover:text-white underline-offset-4 hover:underline transition-colors"
|
|
>
|
|
{apiKey ? 'API Key Set' : 'Enter API Key'}
|
|
</button>
|
|
<button
|
|
onClick={executeRequest}
|
|
disabled={!apiKey || isExecuting}
|
|
className="px-4 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 text-white text-sm font-medium transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{isExecuting ? 'Executing...' : 'Try It'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Inline Response (if any) */}
|
|
{(response || error) && (
|
|
<div className="border-t border-slate-700/50 p-6 bg-slate-950/50">
|
|
<h4 className="text-sm font-semibold text-white mb-3">
|
|
Response
|
|
</h4>
|
|
{error ? (
|
|
<div className="p-4 bg-red-500/10 border border-red-500/40 rounded-xl text-red-400 text-sm">
|
|
<p className="font-semibold mb-1">Error</p>
|
|
<p className="text-xs">{error}</p>
|
|
</div>
|
|
) : (
|
|
<div className={`p-4 rounded-xl ${
|
|
isSuccess
|
|
? 'bg-green-500/5 border border-green-500/30'
|
|
: 'bg-slate-800/50 border border-slate-700'
|
|
}`}>
|
|
{isSuccess && (
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<span className="px-2 py-1 text-xs bg-green-500/20 text-green-400 rounded border border-green-500/40">
|
|
200 Success
|
|
</span>
|
|
</div>
|
|
)}
|
|
<pre className="text-xs text-gray-300 overflow-x-auto max-h-60">
|
|
<code>{JSON.stringify(response, null, 2)}</code>
|
|
</pre>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Expanded Modal */}
|
|
{isExpanded && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm">
|
|
<div className="relative w-[90vw] h-[85vh] bg-slate-900 border border-slate-700/50 rounded-2xl shadow-2xl overflow-hidden flex flex-col">
|
|
{/* Modal Header */}
|
|
<div className="flex items-center justify-between bg-slate-950/80 px-6 py-4 border-b border-slate-700/50">
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`px-3 py-1 rounded text-xs font-bold ${getMethodBadgeStyle(method)}`}>
|
|
{method}
|
|
</div>
|
|
<code className="text-sm text-gray-400">{endpoint}</code>
|
|
</div>
|
|
<span className="text-xs text-gray-500">•</span>
|
|
<p className="text-xs text-gray-500">{description}</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsExpanded(false)}
|
|
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-gray-400 hover:text-white rounded-lg transition-all text-sm font-medium"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
|
|
{/* Two-Panel Layout */}
|
|
<div className="grid grid-cols-2 gap-6 p-6 flex-1 overflow-hidden">
|
|
{/* Left: Code */}
|
|
<div className="flex flex-col h-full min-h-0">
|
|
<div className="flex items-center gap-2 mb-4 flex-wrap min-h-[32px] flex-shrink-0">
|
|
{(['curl', 'javascript', 'python', 'go', 'js-sdk', 'mcp'] as Language[]).map((lang) => (
|
|
<button
|
|
key={lang}
|
|
onClick={() => setLanguage(lang)}
|
|
className={`px-3 py-1.5 text-xs rounded-lg font-medium transition-all border ${
|
|
language === lang
|
|
? 'bg-slate-700 text-white border-slate-600'
|
|
: 'bg-slate-800/50 text-gray-400 border-slate-700/50 hover:border-slate-600 hover:text-gray-300'
|
|
}`}
|
|
>
|
|
{getLanguageLabel(lang)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="border border-slate-700/50 rounded-xl overflow-hidden flex-1 flex flex-col min-h-0">
|
|
<div className="flex items-center justify-between bg-slate-950/80 px-4 py-3 border-b border-slate-700/50 flex-shrink-0">
|
|
<span className="text-xs text-gray-400 font-medium">Code Example</span>
|
|
<button
|
|
onClick={copyCode}
|
|
className="text-xs text-gray-400 hover:text-white transition-colors font-medium"
|
|
>
|
|
Copy
|
|
</button>
|
|
</div>
|
|
<div className="p-4 bg-slate-950/50 overflow-auto flex-1 min-h-0">
|
|
<pre className="text-sm text-gray-300 leading-relaxed">
|
|
<code>{generateCode()}</code>
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right: Response */}
|
|
<div className="flex flex-col h-full min-h-0">
|
|
<h4 className="text-sm font-semibold text-white mb-4 min-h-[32px] flex items-center flex-shrink-0">
|
|
Response
|
|
</h4>
|
|
|
|
<div className="border border-slate-700/50 rounded-xl overflow-hidden flex-1 flex flex-col min-h-0">
|
|
<div className="bg-slate-950/80 px-4 py-3 border-b border-slate-700/50 flex-shrink-0">
|
|
{response && (
|
|
<span
|
|
className={`inline-flex items-center gap-2 px-3 py-1 rounded text-xs font-bold ${
|
|
response.success
|
|
? 'bg-green-500/20 text-green-400 border border-green-500/40'
|
|
: 'bg-red-500/20 text-red-400 border border-red-500/40'
|
|
}`}
|
|
>
|
|
{response.success ? 'Success' : 'Error'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="p-4 bg-slate-950/50 overflow-auto flex-1 min-h-0">
|
|
{error ? (
|
|
<div className="p-4 bg-red-500/10 border border-red-500/40 rounded-xl text-red-400 text-sm">
|
|
<p className="font-semibold mb-1">Error</p>
|
|
<p className="text-xs">{error}</p>
|
|
</div>
|
|
) : response ? (
|
|
<pre className="text-sm text-gray-300 leading-relaxed">
|
|
<code>{JSON.stringify(response, null, 2)}</code>
|
|
</pre>
|
|
) : (
|
|
<div className="flex items-center justify-center h-full">
|
|
<div className="text-center">
|
|
<p className="text-sm text-gray-500">
|
|
Click <strong className="text-white">"Try It"</strong> below to see the response
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Try It Button in Expanded View */}
|
|
<button
|
|
onClick={executeRequest}
|
|
disabled={!apiKey || isExecuting}
|
|
className="mt-4 px-4 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 text-white text-sm font-medium transition-all disabled:opacity-50 disabled:cursor-not-allowed flex-shrink-0"
|
|
>
|
|
{isExecuting ? 'Executing...' : 'Try It'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
};
|