270 lines
11 KiB
TypeScript
270 lines
11 KiB
TypeScript
import { ReactNode, useState } from 'react';
|
|
import Button from '../ui/button/Button';
|
|
import { fetchAPI } from '../../services/api';
|
|
|
|
interface ValidationCardProps {
|
|
title: string;
|
|
description?: string;
|
|
integrationId: string;
|
|
icon?: ReactNode;
|
|
}
|
|
|
|
interface TestResult {
|
|
success: boolean;
|
|
message: string;
|
|
model_used?: string;
|
|
response?: string;
|
|
tokens_used?: string;
|
|
total_tokens?: number;
|
|
cost?: string;
|
|
full_response?: any;
|
|
}
|
|
|
|
/**
|
|
* Validation Card Component
|
|
* Two-way response validation testing for OpenAI API
|
|
* Matches reference plugin implementation exactly
|
|
*/
|
|
export default function ValidationCard({
|
|
title,
|
|
description,
|
|
integrationId,
|
|
icon,
|
|
}: ValidationCardProps) {
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [testResult, setTestResult] = useState<TestResult | null>(null);
|
|
const [withResponse, setWithResponse] = useState(false);
|
|
|
|
// Support OpenAI and Runware
|
|
if (integrationId !== 'openai' && integrationId !== 'runware') {
|
|
return null;
|
|
}
|
|
|
|
const testApiConnection = async (withResponseTest: boolean = false) => {
|
|
setIsLoading(true);
|
|
setWithResponse(withResponseTest);
|
|
setTestResult(null);
|
|
|
|
try {
|
|
// Get saved settings to get API key and model
|
|
const settingsData = await fetchAPI(`/v1/system/settings/integrations/${integrationId}/`);
|
|
|
|
let apiKey = '';
|
|
let model = 'gpt-4.1';
|
|
|
|
if (settingsData.success && settingsData.data) {
|
|
apiKey = settingsData.data.apiKey || '';
|
|
model = settingsData.data.model || 'gpt-4.1';
|
|
}
|
|
|
|
if (!apiKey) {
|
|
setTestResult({
|
|
success: false,
|
|
message: 'API key not configured. Please configure your API key in settings first.',
|
|
});
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
// Call test endpoint
|
|
// For Runware, we don't need with_response or model config
|
|
const requestBody: any = {
|
|
apiKey: apiKey,
|
|
};
|
|
|
|
if (integrationId === 'openai') {
|
|
requestBody.config = {
|
|
model: model,
|
|
with_response: withResponseTest,
|
|
};
|
|
}
|
|
|
|
const data = await fetchAPI(`/v1/system/settings/integrations/${integrationId}/test/`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(requestBody),
|
|
});
|
|
|
|
if (data.success) {
|
|
setTestResult({
|
|
success: true,
|
|
message: data.message || 'API connection successful!',
|
|
model_used: data.model_used || data.model,
|
|
response: data.response,
|
|
tokens_used: data.tokens_used,
|
|
total_tokens: data.total_tokens,
|
|
cost: data.cost,
|
|
full_response: data.full_response || {
|
|
image_url: data.image_url,
|
|
provider: data.provider,
|
|
size: data.size,
|
|
},
|
|
});
|
|
} else {
|
|
setTestResult({
|
|
success: false,
|
|
message: data.error || data.message || 'API connection failed',
|
|
});
|
|
}
|
|
} catch (error: any) {
|
|
setTestResult({
|
|
success: false,
|
|
message: `API connection failed: ${error.message || 'Unknown error'}`,
|
|
});
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<article className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/3">
|
|
<div className="relative p-5 pb-6">
|
|
<h3 className="mb-2 text-base font-semibold text-gray-800 dark:text-white/90">
|
|
{title}
|
|
</h3>
|
|
{description && (
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
{description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="border-t border-gray-200 p-5 dark:border-gray-800">
|
|
<div className="space-y-4">
|
|
{/* Test Buttons */}
|
|
<div className="flex gap-3">
|
|
{integrationId === 'openai' ? (
|
|
<>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => testApiConnection(false)}
|
|
disabled={isLoading}
|
|
className="flex-1"
|
|
>
|
|
{isLoading && !withResponse ? 'Testing...' : 'Test OpenAI Connection'}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => testApiConnection(true)}
|
|
disabled={isLoading}
|
|
className="flex-1"
|
|
>
|
|
{isLoading && withResponse ? 'Testing...' : 'Test OpenAI Response (Ping)'}
|
|
</Button>
|
|
</>
|
|
) : (
|
|
// Runware: Single button for 128x128 image generation validation
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => testApiConnection(false)}
|
|
disabled={isLoading}
|
|
className="flex-1"
|
|
>
|
|
{isLoading ? 'Testing...' : 'Test Runware Connection'}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Test Results */}
|
|
{testResult && (
|
|
<div className="space-y-3">
|
|
{/* Success Message */}
|
|
{testResult.success && (
|
|
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
|
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
|
</svg>
|
|
<span className="text-sm font-medium">{testResult.message}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Error Message */}
|
|
{!testResult.success && (
|
|
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
|
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
</svg>
|
|
<span className="text-sm font-medium">{testResult.message}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Detailed Results Box */}
|
|
{testResult.success && (
|
|
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-500 p-4 rounded">
|
|
<div className="space-y-2 text-sm">
|
|
{integrationId === 'openai' && withResponse ? (
|
|
// OpenAI response test details
|
|
<>
|
|
<div>
|
|
<strong className="text-gray-700 dark:text-gray-300">Model Used:</strong>{' '}
|
|
<span className="text-gray-900 dark:text-white font-mono-custom">{testResult.model_used || 'N/A'}</span>
|
|
</div>
|
|
<div>
|
|
<strong className="text-gray-700 dark:text-gray-300">Expected:</strong>{' '}
|
|
<span className="text-gray-900 dark:text-white">"OK! Ping Received"</span>
|
|
</div>
|
|
<div>
|
|
<strong className="text-gray-700 dark:text-gray-300">Actual Response:</strong>{' '}
|
|
<span className="text-gray-900 dark:text-white">"{testResult.response || 'N/A'}"</span>
|
|
</div>
|
|
<div>
|
|
<strong className="text-gray-700 dark:text-gray-300">Token Limit Sent:</strong>{' '}
|
|
<span className="text-gray-900 dark:text-white">N/A (from your settings)</span>
|
|
</div>
|
|
<div>
|
|
<strong className="text-gray-700 dark:text-gray-300">Tokens Used:</strong>{' '}
|
|
<span className="text-gray-900 dark:text-white">{testResult.tokens_used || 'N/A'} (input/output)</span>
|
|
</div>
|
|
<div>
|
|
<strong className="text-gray-700 dark:text-gray-300">Total Tokens:</strong>{' '}
|
|
<span className="text-gray-900 dark:text-white">{testResult.total_tokens || 'N/A'}</span>
|
|
</div>
|
|
<div>
|
|
<strong className="text-gray-700 dark:text-gray-300">Cost:</strong>{' '}
|
|
<span className="text-gray-900 dark:text-white">{testResult.cost || '$0.0000'}</span>
|
|
</div>
|
|
</>
|
|
) : integrationId === 'runware' ? (
|
|
// Runware image generation test details
|
|
<>
|
|
<div>
|
|
<strong className="text-gray-700 dark:text-gray-300">Provider:</strong>{' '}
|
|
<span className="text-gray-900 dark:text-white">Runware</span>
|
|
</div>
|
|
<div>
|
|
<strong className="text-gray-700 dark:text-gray-300">Model:</strong>{' '}
|
|
<span className="text-gray-900 dark:text-white font-mono-custom">{testResult.model_used || 'runware:97@1'}</span>
|
|
</div>
|
|
<div>
|
|
<strong className="text-gray-700 dark:text-gray-300">Image Size:</strong>{' '}
|
|
<span className="text-gray-900 dark:text-white">128 x 128 (test image)</span>
|
|
</div>
|
|
<div>
|
|
<strong className="text-gray-700 dark:text-gray-300">Cost:</strong>{' '}
|
|
<span className="text-gray-900 dark:text-white">{testResult.cost || '$0.0090'}</span>
|
|
</div>
|
|
{testResult.full_response?.image_url && (
|
|
<div>
|
|
<strong className="text-gray-700 dark:text-gray-300">Test Image:</strong>{' '}
|
|
<a
|
|
href={testResult.full_response.image_url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-blue-600 dark:text-blue-400 hover:underline"
|
|
>
|
|
View Image
|
|
</a>
|
|
</div>
|
|
)}
|
|
</>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|