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 SiteIntegrationsSection from '../../components/integration/SiteIntegrationsSection'; 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; featured_image_size?: string; // e.g., '1280x832', '1024x1024' desktop_image_size?: string; // e.g., '1024x1024', '512x512' } 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, featured_image_size: '1024x1024', // Default, will be set based on provider/model desktop_image_size: '1024x1024', // Default, will be set based on provider/model }, }); 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, 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 if (!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 (uses platform API key) try { // Build request body based on integration type const requestBody: any = {}; // 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 data = await fetchAPI(`/v1/system/settings/integrations/${integrationId}/test/`, { method: 'POST', body: JSON.stringify(requestBody), }); // fetchAPI extracts the data field and throws on error // If we get here without error, validation was successful setValidationStatuses(prev => ({ ...prev, [integrationId]: 'success', })); } 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 model = integration.model; // Validate with current state (fire and forget - don't await) validateIntegration(id, enabled, 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.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]); const loadIntegrationSettings = async () => { try { const integrationIds = ['openai', 'runware', 'image_generation']; const promises = integrationIds.map(async (id) => { try { // fetchAPI extracts 'data' field from {success: true, data: {...}} // So 'data' here is the actual config object {id, enabled, apiKey, ...} const data = await fetchAPI(`/v1/system/settings/integrations/${id}/`); if (data && typeof data === 'object') { return { id, config: 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]; setIsTesting(true); // Set validation status to pending if (selectedIntegration) { setValidationStatuses(prev => ({ ...prev, [selectedIntegration]: 'pending', })); } try { // Test uses platform API key (no apiKey parameter needed) // fetchAPI extracts data from unified format {success: true, data: {...}} // So data is the extracted response payload const data = await fetchAPI(`/v1/system/settings/integrations/${selectedIntegration}/test/`, { method: 'POST', body: JSON.stringify({ config: config, }), }); // fetchAPI extracts data from unified format, so data is the response payload if (data && typeof data === 'object') { // Success response - data contains message, response, tokens_used, etc. 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('Invalid response format'); } } 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') { // Determine default sizes based on provider/model const currentService = config.service || 'openai'; const currentModel = currentService === 'openai' ? (config.model || 'dall-e-3') : (config.runwareModel || 'runware:97@1'); const availableSizes = getImageSizes(currentService, currentModel); const defaultFeaturedSize = availableSizes.length > 0 ? availableSizes[0].value : '1024x1024'; const defaultDesktopSize = availableSizes.length > 0 ? availableSizes[0].value : '1024x1024'; configToSave = { ...config, provider: config.service || config.provider || 'openai', // Map service to provider for backend // Ensure model is set correctly based on service model: currentService === 'openai' ? (config.model || 'dall-e-3') : (currentService === '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, featured_image_size: config.featured_image_size || defaultFeaturedSize, desktop_image_size: config.desktop_image_size || defaultDesktopSize, }; } const data = await fetchAPI(`/v1/system/settings/integrations/${selectedIntegration}/save/`, { method: 'POST', body: JSON.stringify(configToSave), }); // fetchAPI extracts the data field and throws on error // If we get here without error, save was successful toast.success('Settings saved successfully'); setShowSettingsModal(false); // Reload settings - use setTimeout to avoid state update during render setTimeout(() => { loadIntegrationSettings().then(() => { // Trigger validation after settings are reloaded setTimeout(() => validateEnabledIntegrations(), 300); }).catch(err => { console.error('Error reloading settings after save:', err); }); }, 100); } 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: 'Model', value: config.model || 'gpt-4o-mini' }, { label: 'Status', value: config.using_global ? 'Using platform defaults' : 'Custom settings' }, ]; } else if (integrationId === 'runware') { return [ { label: 'App Name', value: 'Runware API' }, { label: 'Status', value: config.using_global ? 'Using platform defaults' : 'Custom settings' }, ]; } else if (integrationId === 'image_generation') { const service = config.service || 'openai'; const modelDisplay = service === 'openai' ? (config.model || config.imageModel || 'dall-e-3') : (config.runwareModel || 'runware:97@1'); return [ { label: 'Service', value: service === 'openai' ? 'OpenAI DALL-E' : 'Runware' }, { label: 'Model', value: modelDisplay }, { label: 'Status', value: config.using_global ? 'Using platform defaults' : 'Custom settings' }, ]; } return []; }; // Get available image sizes with prices based on provider and model const getImageSizes = useCallback((provider: string, model: string) => { if (provider === 'runware') { return [ { value: '1280x832', label: '1280×832 pixels - $0.009', price: 0.009 }, { value: '1024x1024', label: '1024×1024 pixels - $0.009', price: 0.009 }, { value: '512x512', label: '512×512 pixels - $0.006', price: 0.006 }, ]; } else if (provider === 'openai') { if (model === 'dall-e-2') { return [ { value: '256x256', label: '256×256 pixels - $0.016', price: 0.016 }, { value: '512x512', label: '512×512 pixels - $0.018', price: 0.018 }, { value: '1024x1024', label: '1024×1024 pixels - $0.02', price: 0.02 }, ]; } else if (model === 'dall-e-3') { return [ { value: '1024x1024', label: '1024×1024 pixels - $0.04', price: 0.04 }, ]; } } // Default fallback return [ { value: '1024x1024', label: '1024×1024 pixels', price: 0 }, ]; }, []); const getSettingsFields = useCallback((integrationId: string): FormField[] => { const config = integrations[integrationId]; if (integrationId === 'openai') { return [ { 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' }, { value: 'gpt-5.1', label: 'GPT-5.1 - $1.25 / $10.00 per 1M tokens (16K)' }, { value: 'gpt-5.2', label: 'GPT-5.2 - $1.75 / $14.00 per 1M tokens (16K)' }, ], }, ]; } else if (integrationId === 'runware') { return [ // Runware doesn't have model selection, just using platform API key ]; } 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: 'Hi Dream Full - Standard' }, { value: 'civitai:618692@691639', label: 'Bria 3.2 - Premium' }, ], }); } // 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]); // Update image sizes when service/model changes useEffect(() => { if (selectedIntegration !== 'image_generation' || !showSettingsModal) return; const config = integrations[selectedIntegration]; if (!config) return; const service = config.service || 'openai'; const model = service === 'openai' ? (config.model || 'dall-e-3') : (config.runwareModel || 'runware:97@1'); const availableSizes = getImageSizes(service, model); if (availableSizes.length > 0) { const defaultSize = availableSizes[0].value; const currentFeaturedSize = config.featured_image_size; const currentDesktopSize = config.desktop_image_size; // Check if current sizes are valid for the new provider/model const validSizes = availableSizes.map(s => s.value); const needsUpdate = !currentFeaturedSize || !validSizes.includes(currentFeaturedSize) || !currentDesktopSize || !validSizes.includes(currentDesktopSize); if (needsUpdate) { setIntegrations({ ...integrations, [selectedIntegration]: { ...config, featured_image_size: validSizes.includes(currentFeaturedSize || '') ? currentFeaturedSize : defaultSize, desktop_image_size: validSizes.includes(currentDesktopSize || '') ? currentDesktopSize : defaultSize, }, }); } } }, [integrations[selectedIntegration]?.service, integrations[selectedIntegration]?.model, integrations[selectedIntegration]?.runwareModel, selectedIntegration, showSettingsModal, getImageSizes]); // 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) - Selectable */}
Featured Image
Always Enabled
{ setIntegrations({ ...integrations, [selectedIntegration]: { ...integrations[selectedIntegration], featured_image_size: value, }, }); }} className="w-full" />
{/* Row 2: Desktop & Mobile Images (2 columns) */}
{/* Desktop Images Checkbox with Size Selector */}
{ setIntegrations({ ...integrations, [selectedIntegration]: { ...integrations[selectedIntegration], desktop_enabled: checked, }, }); }} />
{integrations[selectedIntegration]?.desktop_enabled !== false && ( { setIntegrations({ ...integrations, [selectedIntegration]: { ...integrations[selectedIntegration], desktop_image_size: value, }, }); }} className="w-full" /> )}
{/* Mobile Images Checkbox - Fixed to 512x512 */}
{ setIntegrations({ ...integrations, [selectedIntegration]: { ...integrations[selectedIntegration], mobile_enabled: checked, }, }); }} />
512×512 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 ( <>
{/* Platform API Keys Info */} {/* 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 const model = data?.model || integrations.openai.model; // Validate with current enabled state and model validateIntegration('openai', enabled, 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': 'Hi Dream Full - Standard', 'civitai:618692@691639': 'Bria 3.2 - Premium', }; return modelDisplayNames[integrations.image_generation.runwareModel] || integrations.image_generation.runwareModel; })() : undefined } onToggleSuccess={(enabled, data) => { // Refresh status circle when toggle changes // Validate with current enabled state validateIntegration('runware', enabled); }} 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 */}
} /> } />
{/* Site Integrations Section */}
{/* 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 } /> )} ); }