import { useState, useEffect, useCallback, useMemo } from 'react'; import PageMeta from '../../components/common/PageMeta'; import IntegrationCard from '../../components/common/IntegrationCard'; import ValidationCard from '../../components/common/ValidationCard'; import ImageGenerationCard from '../../components/common/ImageGenerationCard'; import ImageResultCard from '../../components/common/ImageResultCard'; import ImageServiceCard from '../../components/common/ImageServiceCard'; import { Modal } from '../../components/ui/modal'; import FormModal, { FormField } from '../../components/common/FormModal'; import Button from '../../components/ui/button/Button'; import Checkbox from '../../components/form/input/Checkbox'; import Label from '../../components/form/Label'; import SelectDropdown from '../../components/form/SelectDropdown'; import { useToast } from '../../components/ui/toast/ToastContainer'; import Alert from '../../components/ui/alert/Alert'; import { fetchAPI } from '../../services/api'; // OpenAI Icon SVG const OpenAIIcon = () => ( ); // Runware Icon SVG (simplified placeholder - replace with actual) const RunwareIcon = () => ( ); // GSC Icon SVG (Google Search Console) const GSCIcon = () => ( ); interface IntegrationConfig { id: string; enabled: boolean; apiKey?: string; clientId?: string; clientSecret?: string; authBaseUri?: string; appName?: string; model?: string; // Image generation service settings (separate from API integrations) service?: string; // 'openai' or 'runware' provider?: string; // Alias for service (used in backend) imageModel?: string; // OpenAI model: 'dall-e-3', 'dall-e-2', etc. runwareModel?: string; // Runware model: 'runware:97@1', etc. // Image generation settings image_type?: string; // 'realistic', 'artistic', 'cartoon' max_in_article_images?: number; // 1-5 image_format?: string; // 'webp', 'jpg', 'png' desktop_enabled?: boolean; mobile_enabled?: boolean; } export default function Integration() { const toast = useToast(); const [integrations, setIntegrations] = useState>({ openai: { id: 'openai', enabled: false, apiKey: '', model: 'gpt-4.1', }, runware: { id: 'runware', enabled: false, apiKey: '', }, image_generation: { id: 'image_generation', enabled: false, service: 'openai', // 'openai' or 'runware' provider: 'openai', // Alias for backend model: 'dall-e-3', // OpenAI model if service is 'openai' runwareModel: 'runware:97@1', // Runware model if service is 'runware' image_type: 'realistic', // 'realistic', 'artistic', 'cartoon' max_in_article_images: 2, // 1-5 image_format: 'webp', // 'webp', 'jpg', 'png' desktop_enabled: true, mobile_enabled: true, }, }); const [selectedIntegration, setSelectedIntegration] = useState(null); const [showDetailsModal, setShowDetailsModal] = useState(false); const [showSettingsModal, setShowSettingsModal] = useState(false); const [isSaving, setIsSaving] = useState(false); const [isTesting, setIsTesting] = useState(false); // Validation status for each integration: 'not_configured' | 'pending' | 'success' | 'error' const [validationStatuses, setValidationStatuses] = useState>({ openai: 'not_configured', runware: 'not_configured', image_generation: 'not_configured', }); /** * Validate a single integration's status * This is the core validation function that checks enabled state and API availability */ const validateIntegration = useCallback(async ( integrationId: string, enabled: boolean, apiKey?: string, model?: string ) => { const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api'; // Only validate OpenAI and Runware (GSC doesn't have validation endpoint) if (!['openai', 'runware'].includes(integrationId)) { return; } // Check if integration is enabled and has API key configured const hasApiKey = apiKey && apiKey.trim() !== ''; if (!hasApiKey || !enabled) { // Not configured or disabled - set status accordingly setValidationStatuses(prev => ({ ...prev, [integrationId]: 'not_configured', })); return; } // Set pending status setValidationStatuses(prev => ({ ...prev, [integrationId]: 'pending', })); // Test connection asynchronously try { // Build request body based on integration type const requestBody: any = { apiKey: apiKey, }; // OpenAI needs model in config, Runware doesn't if (integrationId === 'openai') { requestBody.config = { model: model || 'gpt-4.1', with_response: false, // Simple connection test for status validation }; } const response = await fetch( `${API_BASE_URL}/v1/system/settings/integrations/${integrationId}/test/`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify(requestBody), } ); const data = await response.json(); if (response.ok && data.success) { // Validation successful setValidationStatuses(prev => ({ ...prev, [integrationId]: 'success', })); } else { // Validation failed console.error(`Validation failed for ${integrationId}:`, data.error || data.message); setValidationStatuses(prev => ({ ...prev, [integrationId]: 'error', })); } } catch (error: any) { console.error(`Error validating ${integrationId}:`, error); setValidationStatuses(prev => ({ ...prev, [integrationId]: 'error', })); } }, []); /** * Validate all enabled and configured integrations * Used on page load to validate all integrations */ const validateEnabledIntegrations = useCallback(async () => { // Use functional update to read latest state without adding dependencies setIntegrations((currentIntegrations) => { // Validate each integration ['openai', 'runware'].forEach((id) => { const integration = currentIntegrations[id]; if (!integration) return; const enabled = integration.enabled === true; const apiKey = integration.apiKey; const model = integration.model; // Validate with current state (fire and forget - don't await) validateIntegration(id, enabled, apiKey, model); }); // Return unchanged - we're just reading state return currentIntegrations; }); }, [validateIntegration]); // Load integration settings on mount useEffect(() => { loadIntegrationSettings(); }, []); // Validate integrations after settings are loaded or changed (debounced to prevent excessive validation) useEffect(() => { // Only validate if integrations have been loaded (not initial empty state) const hasLoadedData = Object.values(integrations).some(integ => integ.apiKey !== undefined || integ.enabled !== undefined ); if (!hasLoadedData) return; // Debounce validation to prevent excessive API calls const timeoutId = setTimeout(() => { validateEnabledIntegrations(); }, 500); // Wait 500ms after last change return () => clearTimeout(timeoutId); // eslint-disable-next-line react-hooks/exhaustive-deps }, [integrations.openai.enabled, integrations.runware.enabled, integrations.openai.apiKey, integrations.runware.apiKey]); const loadIntegrationSettings = async () => { try { const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api'; const integrationIds = ['openai', 'runware', 'image_generation']; const promises = integrationIds.map(async (id) => { try { const response = await fetch( `${API_BASE_URL}/v1/system/settings/integrations/${id}/`, { credentials: 'include' } ); if (response.ok) { const data = await response.json(); if (data.success && data.data) { return { id, config: data.data }; } } return { id, config: null }; } catch (error) { console.error(`Error loading ${id} settings:`, error); return { id, config: null }; } }); const results = await Promise.all(promises); // Use functional update to avoid stale closure issues setIntegrations((prevIntegrations) => { const updatedIntegrations = { ...prevIntegrations }; results.forEach(({ id, config }) => { if (config && prevIntegrations[id]) { updatedIntegrations[id] = { ...prevIntegrations[id], ...config, enabled: config.enabled !== undefined ? config.enabled : prevIntegrations[id].enabled, }; } }); return updatedIntegrations; }); } catch (error) { console.error('Error loading integration settings:', error); } }; // Note: handleToggle removed - now using built-in persistence via IntegrationCard // The IntegrationCard component with integrationId prop handles toggle persistence automatically const handleSettings = (integrationId: string) => { setSelectedIntegration(integrationId); setShowSettingsModal(true); }; const handleDetails = (integrationId: string) => { setSelectedIntegration(integrationId); setShowDetailsModal(true); }; const handleTestConnection = async () => { if (!selectedIntegration) return; // Only OpenAI and Runware support testing if (selectedIntegration !== 'openai' && selectedIntegration !== 'runware') { toast.warning('Connection testing is only available for OpenAI and Runware'); return; } const config = integrations[selectedIntegration]; const apiKey = config.apiKey; if (!apiKey) { toast.error('Please enter an API key first'); return; } setIsTesting(true); // Set validation status to pending if (selectedIntegration) { setValidationStatuses(prev => ({ ...prev, [selectedIntegration]: 'pending', })); } try { const data = await fetchAPI(`/v1/system/settings/integrations/${selectedIntegration}/test/`, { method: 'POST', body: JSON.stringify({ apiKey, config: config, }), }); if (data.success) { toast.success(data.message || 'API connection test successful!'); if (data.response) { toast.info(`Response: ${data.response}`); } if (data.tokens_used) { toast.info(`Tokens used: ${data.tokens_used}`); } // Update validation status to success if (selectedIntegration) { setValidationStatuses(prev => ({ ...prev, [selectedIntegration]: 'success', })); } } else { throw new Error(data.error || 'Connection test failed'); } } catch (error: any) { console.error('Error testing connection:', error); toast.error(`Connection test failed: ${error.message || 'Unknown error'}`); // Update validation status to error if (selectedIntegration) { setValidationStatuses(prev => ({ ...prev, [selectedIntegration]: 'error', })); } } finally { setIsTesting(false); } }; const handleSaveSettings = async () => { if (!selectedIntegration) return; setIsSaving(true); try { const config = integrations[selectedIntegration]; // For image_generation, map service to provider and ensure all settings are included let configToSave = { ...config }; if (selectedIntegration === 'image_generation') { configToSave = { ...config, provider: config.service || config.provider || 'openai', // Map service to provider for backend // Ensure model is set correctly based on service model: config.service === 'openai' ? (config.model || 'dall-e-3') : (config.service === 'runware' ? (config.runwareModel || 'runware:97@1') : config.model), // Ensure all image settings have defaults image_type: config.image_type || 'realistic', max_in_article_images: config.max_in_article_images || 2, image_format: config.image_format || 'webp', desktop_enabled: config.desktop_enabled !== undefined ? config.desktop_enabled : true, mobile_enabled: config.mobile_enabled !== undefined ? config.mobile_enabled : true, }; } const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api'; const endpoint = `${API_BASE_URL}/v1/system/settings/integrations/${selectedIntegration}/save/`; const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify(configToSave), }); // Check if response is HTML (error page) const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json')) { const text = await response.text(); if (text.trim().startsWith(' { loadIntegrationSettings().then(() => { // Trigger validation after settings are reloaded setTimeout(() => validateEnabledIntegrations(), 300); }).catch(err => { console.error('Error reloading settings after save:', err); }); }, 100); } else { throw new Error(data.error || 'Failed to save settings'); } } catch (error: any) { console.error('Error saving integration settings:', error); toast.error(`Failed to save settings: ${error.message || 'Unknown error'}`); } finally { setIsSaving(false); } }; const getDetailsData = (integrationId: string) => { const config = integrations[integrationId]; if (integrationId === 'openai') { return [ { label: 'App Name', value: 'OpenAI API' }, { label: 'API Key', value: config.apiKey ? `${config.apiKey.substring(0, 20)}...` : 'Not configured' }, { label: 'Model', value: config.model || 'Not set' }, ]; } else if (integrationId === 'runware') { return [ { label: 'App Name', value: 'Runware API' }, { label: 'API Key', value: config.apiKey ? `${config.apiKey.substring(0, 20)}...` : 'Not configured' }, ]; } else if (integrationId === 'image_generation') { const service = config.service || 'openai'; const modelDisplay = service === 'openai' ? (config.model || 'Not set') : (config.runwareModel || 'Not set'); return [ { label: 'Service', value: service === 'openai' ? 'OpenAI' : 'Runware' }, { label: 'Model', value: modelDisplay }, ]; } return []; }; const getSettingsFields = useCallback((integrationId: string): FormField[] => { const config = integrations[integrationId]; if (integrationId === 'openai') { return [ { key: 'apiKey', label: 'OpenAI API Key', type: 'password', value: config.apiKey || '', onChange: (value) => { setIntegrations({ ...integrations, [integrationId]: { ...config, apiKey: value }, }); }, placeholder: 'Enter your OpenAI API key', required: true, }, { key: 'model', label: 'AI Model', type: 'select', value: config.model || 'gpt-4.1', onChange: (value) => { setIntegrations({ ...integrations, [integrationId]: { ...config, model: value }, }); }, options: [ { value: 'gpt-4.1', label: 'GPT-4.1 - $2.00 / $8.00 per 1M tokens' }, { value: 'gpt-4o-mini', label: 'GPT-4o mini - $0.15 / $0.60 per 1M tokens' }, { value: 'gpt-4o', label: 'GPT-4o - $2.50 / $10.00 per 1M tokens' }, ], }, ]; } else if (integrationId === 'runware') { return [ { key: 'apiKey', label: 'Runware API Key', type: 'password', value: config.apiKey || '', onChange: (value) => { setIntegrations({ ...integrations, [integrationId]: { ...config, apiKey: value }, }); }, placeholder: 'Enter your Runware API key', required: true, }, ]; } else if (integrationId === 'image_generation') { const service = config.service || 'openai'; const fields: FormField[] = [ { key: 'service', label: 'Image Generation Service', type: 'select', value: service, onChange: (value) => { setIntegrations({ ...integrations, [integrationId]: { ...config, service: value, // Reset model when service changes model: value === 'openai' ? 'dall-e-3' : undefined, runwareModel: value === 'runware' ? 'runware:97@1' : undefined, }, }); }, options: [ { value: 'openai', label: 'OpenAI - Multiple models available' }, { value: 'runware', label: 'Runware - $0.009 per image' }, ], }, ]; // Add provider-specific model selector if (service === 'openai') { fields.push({ key: 'model', label: 'OpenAI Image Model', type: 'select', value: config.model || 'dall-e-3', onChange: (value) => { setIntegrations({ ...integrations, [integrationId]: { ...config, model: value }, }); }, options: [ { value: 'dall-e-3', label: 'DALL·E 3 - $0.040 per image' }, { value: 'dall-e-2', label: 'DALL·E 2 - $0.020 per image' }, // Note: gpt-image-1 and gpt-image-1-mini are not valid for OpenAI's /v1/images/generations endpoint // They are not currently supported by OpenAI's image generation API // Only dall-e-3 and dall-e-2 are supported ], }); } else if (service === 'runware') { fields.push({ key: 'runwareModel', label: 'Runware Model', type: 'select', value: config.runwareModel || 'runware:97@1', onChange: (value) => { setIntegrations({ ...integrations, [integrationId]: { ...config, runwareModel: value }, }); }, options: [ { value: 'runware:97@1', label: 'HiDream-I1 Full - $0.009 per image' }, { value: 'runware:gen3a_turbo', label: 'Gen3a Turbo - $0.009 per image' }, { value: 'runware:gen3a', label: 'Gen3a - $0.009 per image' }, ], }); } // Note: API key is configured in OpenAI/Runware integration cards, not here // Add Image Generation Settings fields (no API key field) fields.push( { key: 'image_type', label: 'Image Type', type: 'select', value: config.image_type || 'realistic', onChange: (value) => { setIntegrations({ ...integrations, [integrationId]: { ...config, image_type: value }, }); }, options: [ { value: 'realistic', label: 'Realistic' }, { value: 'artistic', label: 'Artistic' }, { value: 'cartoon', label: 'Cartoon' }, ], }, { key: 'max_in_article_images', label: 'Max In-Article Images', type: 'select', value: String(config.max_in_article_images || 2), onChange: (value) => { setIntegrations({ ...integrations, [integrationId]: { ...config, max_in_article_images: parseInt(value) }, }); }, options: [ { value: '1', label: '1 Image' }, { value: '2', label: '2 Images' }, { value: '3', label: '3 Images' }, { value: '4', label: '4 Images' }, { value: '5', label: '5 Images' }, ], }, { key: 'image_format', label: 'Image Format', type: 'select', value: config.image_format || 'webp', onChange: (value) => { setIntegrations({ ...integrations, [integrationId]: { ...config, image_format: value }, }); }, options: [ { value: 'webp', label: 'WEBP' }, { value: 'jpg', label: 'JPG' }, { value: 'png', label: 'PNG' }, ], } ); return fields; } return []; }, [integrations]); // Memoize custom body for image generation modal to prevent infinite loops const imageGenerationCustomBody = useMemo(() => { if (selectedIntegration !== 'image_generation' || !showSettingsModal) return undefined; try { const fields = getSettingsFields(selectedIntegration); const serviceField = fields.find(f => f.key === 'service'); const service = integrations[selectedIntegration]?.service || 'openai'; const modelField = fields.find(f => { return service === 'openai' ? f.key === 'model' : f.key === 'runwareModel'; }); const maxImagesField = fields.find(f => f.key === 'max_in_article_images'); const imageTypeField = fields.find(f => f.key === 'image_type'); const imageFormatField = fields.find(f => f.key === 'image_format'); return (
{/* Row 1: Image Generation Service & Model Selector (2 columns) */}
{/* Service Selector */} {serviceField && (
serviceField.onChange(value)} className="w-full" />
)} {/* Model Selector */} {modelField && (
modelField.onChange(value)} className="w-full" />
)}
{/* Max Images Section */}
{/* Featured Image (full width) */}
Featured Image
1280×832 pixels
Always Enabled
{/* Row 2: Desktop & Mobile Images (2 columns) */}
{/* Desktop Images Checkbox */}
{ setIntegrations({ ...integrations, [selectedIntegration]: { ...integrations[selectedIntegration], desktop_enabled: checked, }, }); }} />
1024×1024 pixels
{/* Mobile Images Checkbox */}
{ setIntegrations({ ...integrations, [selectedIntegration]: { ...integrations[selectedIntegration], mobile_enabled: checked, }, }); }} />
960×1280 pixels
{/* Row 3: Max In-Article Images, Image Type & Image Format (3 columns) */}
{/* Max In-Article Images */} {maxImagesField && (
maxImagesField.onChange(value)} className="w-full" />
)} {/* Image Type */} {imageTypeField && (
imageTypeField.onChange(value)} className="w-full" />
)} {/* Image Format */} {imageFormatField && (
imageFormatField.onChange(value)} className="w-full" />
)}
); } catch (error) { console.error('Error rendering image generation form:', error); return
Error loading form. Please refresh the page.
; } }, [selectedIntegration, integrations, showSettingsModal, getSettingsFields]); return ( <>
{/* Integration Cards with Validation Cards */}
{/* OpenAI Integration + Validation */}
} title="OpenAI" description="AI-powered content generation and analysis with DALL-E image generation" validationStatus={validationStatuses.openai} integrationId="openai" onToggleSuccess={(enabled, data) => { // Refresh status circle when toggle changes // Use API key from hook's data (most up-to-date) or fallback to integrations state const apiKey = data?.apiKey || integrations.openai.apiKey; const model = data?.model || integrations.openai.model; // Validate with current enabled state and API key validateIntegration('openai', enabled, apiKey, model); }} onSettings={() => handleSettings('openai')} onDetails={() => handleDetails('openai')} /> } />
{/* Runware Integration + Validation */}
} title="Runware" description="High-quality AI image generation with Runware's models ($0.009 per image)" validationStatus={validationStatuses.runware} integrationId="runware" modelName={ integrations.image_generation?.service === 'runware' && integrations.image_generation.runwareModel ? (() => { // Map model ID to display name const modelDisplayNames: Record = { 'runware:97@1': 'HiDream-I1 Full', 'runware:gen3a_turbo': 'Gen3a Turbo', 'runware:gen3a': 'Gen3a', }; return modelDisplayNames[integrations.image_generation.runwareModel] || integrations.image_generation.runwareModel; })() : undefined } onToggleSuccess={(enabled, data) => { // Refresh status circle when toggle changes // Use API key from hook's data (most up-to-date) or fallback to integrations state const apiKey = data?.apiKey || integrations.runware.apiKey; // Validate with current enabled state and API key validateIntegration('runware', enabled, apiKey); }} onSettings={() => handleSettings('runware')} onDetails={() => handleDetails('runware')} /> } />
{/* Image Generation Service Card */}
} title="Image Generation Service" description="Default image generation service and model selection for app-wide use" validationStatus={validationStatuses.image_generation} onSettings={() => handleSettings('image_generation')} onDetails={() => handleDetails('image_generation')} /> {/* AI Integration & Image Generation Testing Info Card */}
{/* Image Generation Testing Cards - 50/50 Split */}
} /> } />
{/* Details Modal */} setShowDetailsModal(false)} className="max-w-2xl" >

Integration details

Check the credentials and settings for your connected app.

{selectedIntegration && getDetailsData(selectedIntegration).map((item, index) => (
{item.label} {item.value}
))}
{/* Settings Modal */} {selectedIntegration && ( <> setShowSettingsModal(false)} onSubmit={handleSaveSettings} title="Integration settings" fields={getSettingsFields(selectedIntegration)} submitLabel="Save Changes" cancelLabel="Close" isLoading={isSaving} customBody={imageGenerationCustomBody} customFooter={ (selectedIntegration === 'openai' || selectedIntegration === 'runware') ? (
) : undefined } /> )} ); }