Files
igny8/frontend/src/components/common/ValidationCard.tsx

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>
);
}