lot of messs
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { ReactNode, useState, useEffect } from 'react';
|
||||
import Switch from '../form/switch/Switch';
|
||||
import Button from '../ui/button/Button';
|
||||
import { usePersistentToggle } from '../../hooks/usePersistentToggle';
|
||||
import { useToast } from '../ui/toast/ToastContainer';
|
||||
|
||||
type ValidationStatus = 'not_configured' | 'pending' | 'success' | 'error';
|
||||
@@ -13,12 +12,12 @@ interface ImageServiceCardProps {
|
||||
validationStatus: ValidationStatus;
|
||||
onSettings: () => void;
|
||||
onDetails: () => void;
|
||||
onToggleSuccess?: (enabled: boolean, data?: any) => void; // Callback when toggle succeeds
|
||||
}
|
||||
|
||||
/**
|
||||
* Image Generation Service Card Component
|
||||
* Manages default image generation service and model selection app-wide
|
||||
* This is separate from individual API integrations (OpenAI/Runware)
|
||||
* Manages default image generation service enable/disable state
|
||||
*/
|
||||
export default function ImageServiceCard({
|
||||
icon,
|
||||
@@ -27,32 +26,20 @@ export default function ImageServiceCard({
|
||||
validationStatus,
|
||||
onSettings,
|
||||
onDetails,
|
||||
onToggleSuccess,
|
||||
}: ImageServiceCardProps) {
|
||||
const toast = useToast();
|
||||
|
||||
// Use built-in persistent toggle for image generation service
|
||||
const persistentToggle = usePersistentToggle({
|
||||
resourceId: 'image_generation',
|
||||
getEndpoint: '/v1/system/settings/integrations/{id}/',
|
||||
saveEndpoint: '/v1/system/settings/integrations/{id}/save/',
|
||||
initialEnabled: false,
|
||||
onToggleSuccess: (enabled) => {
|
||||
toast.success(`Image generation service ${enabled ? 'enabled' : 'disabled'}`);
|
||||
},
|
||||
onToggleError: (error) => {
|
||||
toast.error(`Failed to update image generation service: ${error.message}`);
|
||||
},
|
||||
});
|
||||
|
||||
const enabled = persistentToggle.enabled;
|
||||
const isToggling = persistentToggle.loading;
|
||||
const [imageSettings, setImageSettings] = useState<{ service?: string; model?: string; runwareModel?: string }>({});
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [imageSettings, setImageSettings] = useState<{ service?: string; provider?: string; model?: string; imageModel?: string; runwareModel?: string }>({});
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
|
||||
|
||||
// Load image settings to get provider and model
|
||||
// Load image settings
|
||||
useEffect(() => {
|
||||
const loadSettings = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/v1/system/settings/integrations/image_generation/`,
|
||||
@@ -62,38 +49,67 @@ export default function ImageServiceCard({
|
||||
const data = await response.json();
|
||||
if (data.success && data.data) {
|
||||
setImageSettings(data.data);
|
||||
setEnabled(data.data.enabled || false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading image settings:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
loadSettings();
|
||||
}, [API_BASE_URL, enabled]); // Reload when enabled changes
|
||||
}, [API_BASE_URL]);
|
||||
|
||||
const handleToggle = (newEnabled: boolean) => {
|
||||
persistentToggle.toggle(newEnabled);
|
||||
// Handle toggle
|
||||
const handleToggle = async (newEnabled: boolean) => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/v1/system/settings/integrations/image_generation/save/`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ ...imageSettings, enabled: newEnabled }),
|
||||
}
|
||||
);
|
||||
if (response.ok) {
|
||||
setEnabled(newEnabled);
|
||||
toast.success(`Image generation service ${newEnabled ? 'enabled' : 'disabled'}`);
|
||||
|
||||
// Call onToggleSuccess callback with enabled state and settings data
|
||||
if (onToggleSuccess) {
|
||||
onToggleSuccess(newEnabled, imageSettings);
|
||||
}
|
||||
} else {
|
||||
toast.error('Failed to update image generation service');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling image generation:', error);
|
||||
toast.error('Failed to update image generation service');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Get provider and model display text
|
||||
const getProviderModelText = () => {
|
||||
const service = imageSettings.service || 'openai';
|
||||
const service = imageSettings.service || imageSettings.provider || 'openai';
|
||||
if (service === 'openai') {
|
||||
const model = imageSettings.model || 'dall-e-3';
|
||||
const model = imageSettings.model || imageSettings.imageModel || 'dall-e-3';
|
||||
const modelNames: Record<string, string> = {
|
||||
'dall-e-3': 'DALL·E 3',
|
||||
'dall-e-2': 'DALL·E 2',
|
||||
'gpt-image-1': 'GPT Image 1 (Full)',
|
||||
'gpt-image-1-mini': 'GPT Image 1 Mini',
|
||||
};
|
||||
return `OpenAI ${modelNames[model] || model}`;
|
||||
} else if (service === 'runware') {
|
||||
const model = imageSettings.runwareModel || 'runware:97@1';
|
||||
const model = imageSettings.runwareModel || imageSettings.model || 'runware:97@1';
|
||||
// Map model ID to display name
|
||||
const modelDisplayNames: Record<string, string> = {
|
||||
'runware:97@1': 'HiDream-I1 Full',
|
||||
'runware:gen3a_turbo': 'Gen3a Turbo',
|
||||
'runware:gen3a': 'Gen3a',
|
||||
'runware:100@1': 'Runware 100@1',
|
||||
'runware:101@1': 'Runware 101@1',
|
||||
};
|
||||
const displayName = modelDisplayNames[model] || model;
|
||||
return `Runware ${displayName}`;
|
||||
@@ -177,7 +193,7 @@ export default function ImageServiceCard({
|
||||
<Switch
|
||||
label=""
|
||||
checked={enabled}
|
||||
disabled={isToggling}
|
||||
disabled={loading || isSaving}
|
||||
onChange={handleToggle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -47,11 +47,7 @@ const GSCIcon = () => (
|
||||
interface IntegrationConfig {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
apiKey?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
authBaseUri?: string;
|
||||
appName?: string;
|
||||
// Note: API keys are configured platform-wide in GlobalIntegrationSettings (not user-editable)
|
||||
model?: string;
|
||||
// Image generation service settings (separate from API integrations)
|
||||
service?: string; // 'openai' or 'runware'
|
||||
@@ -74,13 +70,12 @@ export default function Integration() {
|
||||
openai: {
|
||||
id: 'openai',
|
||||
enabled: false,
|
||||
apiKey: '',
|
||||
model: 'gpt-4.1',
|
||||
model: 'gpt-4o-mini',
|
||||
},
|
||||
runware: {
|
||||
id: 'runware',
|
||||
enabled: false,
|
||||
apiKey: '',
|
||||
model: 'runware:97@1',
|
||||
},
|
||||
image_generation: {
|
||||
id: 'image_generation',
|
||||
@@ -105,6 +100,17 @@ export default function Integration() {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
|
||||
// Available models from AIModelConfig
|
||||
const [availableModels, setAvailableModels] = useState<{
|
||||
openai_text: Array<{ value: string; label: string }>;
|
||||
openai_image: Array<{ value: string; label: string }>;
|
||||
runware_image: Array<{ value: string; label: string }>;
|
||||
}>({
|
||||
openai_text: [],
|
||||
openai_image: [],
|
||||
runware_image: [],
|
||||
});
|
||||
|
||||
// Validation status for each integration: 'not_configured' | 'pending' | 'success' | 'error'
|
||||
const [validationStatuses, setValidationStatuses] = useState<Record<string, 'not_configured' | 'pending' | 'success' | 'error'>>({
|
||||
openai: 'not_configured',
|
||||
@@ -124,16 +130,22 @@ export default function Integration() {
|
||||
) => {
|
||||
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)
|
||||
// Image generation doesn't have a test endpoint - just set status based on enabled
|
||||
if (integrationId === 'image_generation') {
|
||||
setValidationStatuses(prev => ({
|
||||
...prev,
|
||||
[integrationId]: enabled ? 'success' : 'not_configured',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Only validate OpenAI and Runware (they have test endpoints)
|
||||
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
|
||||
// If disabled, mark as not_configured (not error!)
|
||||
if (!enabled) {
|
||||
setValidationStatuses(prev => ({
|
||||
...prev,
|
||||
[integrationId]: 'not_configured',
|
||||
@@ -141,40 +153,29 @@ export default function Integration() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set pending status
|
||||
// Integration is enabled - test the connection
|
||||
// Set pending status while testing
|
||||
setValidationStatuses(prev => ({
|
||||
...prev,
|
||||
[integrationId]: 'pending',
|
||||
}));
|
||||
|
||||
// Test connection asynchronously
|
||||
// Test connection asynchronously - send empty body, backend will use global settings
|
||||
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 data = await fetchAPI(`/v1/system/settings/integrations/${integrationId}/test/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(requestBody),
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
// fetchAPI extracts the data field and throws on error
|
||||
// If we get here without error, validation was successful
|
||||
console.log(`✅ Validation successful for ${integrationId}`);
|
||||
setValidationStatuses(prev => ({
|
||||
...prev,
|
||||
[integrationId]: 'success',
|
||||
}));
|
||||
} catch (error: any) {
|
||||
console.error(`Error validating ${integrationId}:`, error);
|
||||
console.error(`❌ Validation failed for ${integrationId}:`, error);
|
||||
setValidationStatuses(prev => ({
|
||||
...prev,
|
||||
[integrationId]: 'error',
|
||||
@@ -189,17 +190,16 @@ export default function Integration() {
|
||||
const validateEnabledIntegrations = useCallback(async () => {
|
||||
// Use functional update to read latest state without adding dependencies
|
||||
setIntegrations((currentIntegrations) => {
|
||||
// Validate each integration
|
||||
['openai', 'runware'].forEach((id) => {
|
||||
// Validate each integration (including image_generation)
|
||||
['openai', 'runware', 'image_generation'].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);
|
||||
validateIntegration(id, enabled, undefined, model);
|
||||
});
|
||||
|
||||
// Return unchanged - we're just reading state
|
||||
@@ -207,16 +207,30 @@ export default function Integration() {
|
||||
});
|
||||
}, [validateIntegration]);
|
||||
|
||||
// Load integration settings on mount
|
||||
// Load available models from backend
|
||||
const loadAvailableModels = async () => {
|
||||
try {
|
||||
const data = await fetchAPI('/v1/system/settings/integrations/available-models/');
|
||||
if (data) {
|
||||
setAvailableModels(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading available models:', error);
|
||||
// Keep default empty arrays
|
||||
}
|
||||
};
|
||||
|
||||
// Load integration settings and available models on mount
|
||||
useEffect(() => {
|
||||
loadIntegrationSettings();
|
||||
loadAvailableModels();
|
||||
}, []);
|
||||
|
||||
// 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
|
||||
integ.enabled !== undefined
|
||||
);
|
||||
if (!hasLoadedData) return;
|
||||
|
||||
@@ -227,7 +241,7 @@ export default function Integration() {
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [integrations.openai.enabled, integrations.runware.enabled, integrations.openai.apiKey, integrations.runware.apiKey]);
|
||||
}, [integrations.openai.enabled, integrations.runware.enabled, integrations.openai.model, integrations.runware.model]);
|
||||
|
||||
const loadIntegrationSettings = async () => {
|
||||
try {
|
||||
@@ -294,12 +308,6 @@ export default function Integration() {
|
||||
}
|
||||
|
||||
const config = integrations[selectedIntegration];
|
||||
const apiKey = config.apiKey;
|
||||
|
||||
if (!apiKey) {
|
||||
toast.error('Please enter an API key first');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsTesting(true);
|
||||
|
||||
@@ -423,13 +431,12 @@ export default function Integration() {
|
||||
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' },
|
||||
{ label: 'Model', value: config.model || 'Not set' },
|
||||
];
|
||||
} else if (integrationId === 'image_generation') {
|
||||
const service = config.service || 'openai';
|
||||
@@ -477,55 +484,48 @@ export default function Integration() {
|
||||
|
||||
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',
|
||||
value: config.model || 'gpt-4o-mini',
|
||||
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)' },
|
||||
],
|
||||
options: availableModels?.openai_text?.length > 0
|
||||
? availableModels.openai_text
|
||||
: [
|
||||
{ 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 [
|
||||
{
|
||||
key: 'apiKey',
|
||||
label: 'Runware API Key',
|
||||
type: 'password',
|
||||
value: config.apiKey || '',
|
||||
key: 'model',
|
||||
label: 'Runware Model',
|
||||
type: 'select',
|
||||
value: config.model || 'runware:97@1',
|
||||
onChange: (value) => {
|
||||
setIntegrations({
|
||||
...integrations,
|
||||
[integrationId]: { ...config, apiKey: value },
|
||||
[integrationId]: { ...config, model: value },
|
||||
});
|
||||
},
|
||||
placeholder: 'Enter your Runware API key',
|
||||
required: true,
|
||||
options: availableModels?.runware_image?.length > 0
|
||||
? availableModels.runware_image
|
||||
: [
|
||||
{ value: 'runware:97@1', label: 'Runware 97@1 - Versatile Model' },
|
||||
{ value: 'runware:100@1', label: 'Runware 100@1 - High Quality' },
|
||||
{ value: 'runware:101@1', label: 'Runware 101@1 - Fast Generation' },
|
||||
],
|
||||
},
|
||||
];
|
||||
} else if (integrationId === 'image_generation') {
|
||||
@@ -569,13 +569,12 @@ export default function Integration() {
|
||||
[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
|
||||
],
|
||||
options: availableModels?.openai_image?.length > 0
|
||||
? availableModels.openai_image
|
||||
: [
|
||||
{ value: 'dall-e-3', label: 'DALL·E 3 - $0.040 per image' },
|
||||
{ value: 'dall-e-2', label: 'DALL·E 2 - $0.020 per image' },
|
||||
],
|
||||
});
|
||||
} else if (service === 'runware') {
|
||||
fields.push({
|
||||
@@ -589,11 +588,13 @@ export default function Integration() {
|
||||
[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' },
|
||||
],
|
||||
options: availableModels?.runware_image?.length > 0
|
||||
? availableModels.runware_image
|
||||
: [
|
||||
{ value: 'runware:97@1', label: 'HiDream-I1 Full - $0.009 per image' },
|
||||
{ value: 'runware:100@1', label: 'Runware 100@1 - High Quality' },
|
||||
{ value: 'runware:101@1', label: 'Runware 101@1 - Fast Generation' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -905,7 +906,7 @@ export default function Integration() {
|
||||
console.error('Error rendering image generation form:', error);
|
||||
return <div className="text-error-500">Error loading form. Please refresh the page.</div>;
|
||||
}
|
||||
}, [selectedIntegration, integrations, showSettingsModal, getSettingsFields]);
|
||||
}, [selectedIntegration, integrations, showSettingsModal, availableModels]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -951,15 +952,15 @@ export default function Integration() {
|
||||
validationStatus={validationStatuses.runware}
|
||||
integrationId="runware"
|
||||
modelName={
|
||||
integrations.image_generation?.service === 'runware' && integrations.image_generation.runwareModel
|
||||
integrations.runware?.enabled && integrations.runware?.model
|
||||
? (() => {
|
||||
// Map model ID to display name
|
||||
const modelDisplayNames: Record<string, string> = {
|
||||
'runware:97@1': 'HiDream-I1 Full',
|
||||
'runware:gen3a_turbo': 'Gen3a Turbo',
|
||||
'runware:gen3a': 'Gen3a',
|
||||
'runware:100@1': 'Runware 100@1',
|
||||
'runware:101@1': 'Runware 101@1',
|
||||
};
|
||||
return modelDisplayNames[integrations.image_generation.runwareModel] || integrations.image_generation.runwareModel;
|
||||
return modelDisplayNames[integrations.runware.model] || integrations.runware.model;
|
||||
})()
|
||||
: undefined
|
||||
}
|
||||
@@ -996,6 +997,12 @@ export default function Integration() {
|
||||
title="Image Generation Service"
|
||||
description="Default image generation service and model selection for app-wide use"
|
||||
validationStatus={validationStatuses.image_generation}
|
||||
onToggleSuccess={(enabled, data) => {
|
||||
// Validate when toggle changes - same pattern as openai/runware
|
||||
const provider = data?.provider || data?.service || 'openai';
|
||||
const model = data?.model || (provider === 'openai' ? 'dall-e-3' : 'runware:97@1');
|
||||
validateIntegration('image_generation', enabled, null, model);
|
||||
}}
|
||||
onSettings={() => handleSettings('image_generation')}
|
||||
onDetails={() => handleDetails('image_generation')}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user