lot of messs

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-23 14:35:41 +00:00
parent edb64824be
commit 38bc015d96
17 changed files with 2448 additions and 303 deletions

View File

@@ -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>

View File

@@ -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')}
/>