image gen mess

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-03 22:31:30 +00:00
parent f518e1751b
commit c4de8994dd
9 changed files with 453 additions and 221 deletions

View File

@@ -51,7 +51,9 @@ export const Modal: React.FC<ModalProps> = ({
const contentClasses = isFullscreen
? "w-full h-full"
: "relative w-full max-w-lg mx-4 rounded-3xl bg-white dark:bg-gray-900 shadow-xl";
: className
? `relative mx-4 rounded-3xl bg-white dark:bg-gray-900 shadow-xl ${className}`
: "relative w-full max-w-lg mx-4 rounded-3xl bg-white dark:bg-gray-900 shadow-xl";
return (
<div className="fixed inset-0 flex items-center justify-center overflow-y-auto modal z-99999">
@@ -63,7 +65,7 @@ export const Modal: React.FC<ModalProps> = ({
)}
<div
ref={modalRef}
className={`${contentClasses} ${className}`}
className={contentClasses}
onClick={(e) => e.stopPropagation()}
>
{showCloseButton && (

View File

@@ -77,74 +77,94 @@ export default function SiteSettings() {
const [contentGenerationSaving, setContentGenerationSaving] = useState(false);
// Image Settings state
const [imageQuality, setImageQuality] = useState<'standard' | 'premium' | 'best'>('premium');
const [imageQuality, setImageQuality] = useState<'basic' | 'quality' | 'premium'>('basic');
const [imageSettings, setImageSettings] = useState({
enabled: true,
service: 'openai' as 'openai' | 'runware',
provider: 'openai',
model: 'dall-e-3',
image_type: 'realistic' as 'realistic' | 'artistic' | 'cartoon',
service: 'runware' as 'runware',
provider: 'runware',
model: 'runware:97@1',
image_type: 'photorealistic' as string,
max_in_article_images: 2,
image_format: 'webp' as 'webp' | 'jpg' | 'png',
featured_image_size: '1024x1024',
});
const [imageSettingsLoading, setImageSettingsLoading] = useState(false);
const [imageSettingsSaving, setImageSettingsSaving] = useState(false);
// Image quality to config mapping
// Updated to use new Runware models via API
const QUALITY_TO_CONFIG: Record<string, { service: 'openai' | 'runware'; model: string }> = {
standard: { service: 'openai', model: 'dall-e-2' },
premium: { service: 'openai', model: 'dall-e-3' },
best: { service: 'runware', model: 'runware:97@1' }, // Uses model-specific landscape size
// Current image provider (from GlobalIntegrationSettings)
const [imageProvider, setImageProvider] = useState<'runware' | 'openai'>('runware');
// Available style options loaded from backend (dynamic based on provider)
const [availableStyles, setAvailableStyles] = useState<Array<{value: string; label: string; description?: string}>>([
{ value: 'photorealistic', label: 'Photorealistic', description: 'Ultra realistic photography style' },
]);
// Image quality to config mapping - Runware models only with model-specific sizes
const QUALITY_TO_CONFIG: Record<string, { service: 'runware'; model: string }> = {
basic: { service: 'runware', model: 'runware:97@1' }, // 6 credits/image
quality: { service: 'runware', model: 'bria:10@1' }, // 10 credits/image
premium: { service: 'runware', model: 'google:4@2' }, // 15 credits/image
};
// Runware model choices with descriptions
// Runware model choices with credits per image (from backend AIModelConfig)
const RUNWARE_MODEL_CHOICES = [
{ value: 'runware:97@1', label: 'Hi Dream Full - Basic', description: 'Fast & affordable' },
{ value: 'bria:10@1', label: 'Bria 3.2 - Quality', description: 'Commercial-safe, licensed data' },
{ value: 'google:4@2', label: 'Nano Banana - Premium', description: 'Best quality, text rendering' },
{ value: 'runware:97@1', label: 'Basic (6 credits/image)', credits: 6 },
{ value: 'bria:10@1', label: 'Quality (10 credits/image)', credits: 10 },
{ value: 'google:4@2', label: 'Premium (15 credits/image)', credits: 15 },
];
const getQualityFromConfig = (service?: string, model?: string): 'standard' | 'premium' | 'best' => {
if (service === 'runware') return 'best';
if (model === 'dall-e-3') return 'premium';
return 'standard';
// OpenAI DALL-E model choices
const DALLE_MODEL_CHOICES = [
{ value: 'dall-e-3', label: 'DALL-E 3 - HD Quality', credits: 40 },
];
// Model-specific style options (image_type attribute compatible with each model)
const getModelStyleOptions = (model: string) => {
// Bria supports medium parameter (photography/art)
if (model === 'bria:10@1') {
return [
{ value: 'photography', label: 'Photography' },
{ value: 'art', label: 'Artistic' },
];
}
// All models support these basic styles via prompt modification
return [
{ value: 'realistic', label: 'Realistic' },
{ value: 'artistic', label: 'Artistic' },
{ value: 'cartoon', label: 'Cartoon' },
];
};
const getImageSizes = (provider: string, model: string) => {
if (provider === 'runware') {
// Model-specific sizes - featured uses landscape, in-article alternates
// Sizes shown are for featured image (landscape)
return [
{ value: '1280x768', label: '1280×768 (Landscape)' },
{ value: '1024x1024', label: '1024×1024 (Square)' },
];
} else if (provider === 'openai') {
if (model === 'dall-e-2') {
return [
{ value: '256x256', label: '256×256 pixels' },
{ value: '512x512', label: '512×512 pixels' },
{ value: '1024x1024', label: '1024×1024 pixels' },
];
} else if (model === 'dall-e-3') {
return [
{ value: '1024x1024', label: '1024×1024 pixels' },
];
}
}
return [{ value: '1024x1024', label: '1024×1024 pixels' }];
const getQualityFromConfig = (service?: string, model?: string): 'basic' | 'quality' | 'premium' => {
if (model === 'google:4@2') return 'premium';
if (model === 'bria:10@1') return 'quality';
return 'basic'; // Default to basic (runware:97@1)
};
// Model-specific landscape sizes (used for featured image)
const MODEL_LANDSCAPE_SIZES: Record<string, string> = {
'runware:97@1': '1280x768', // Hi Dream Full
'bria:10@1': '1344x768', // Bria 3.2
'google:4@2': '1376x768', // Nano Banana
'dall-e-3': '1792x1024', // DALL-E 3
'dall-e-2': '1024x1024', // DALL-E 2 (square only)
};
const getLandscapeSizeForModel = (model: string): string => {
return MODEL_LANDSCAPE_SIZES[model] || (imageProvider === 'openai' ? '1792x1024' : '1280x768');
};
const getCurrentImageConfig = useCallback(() => {
// For OpenAI provider, return the DALL-E model directly
if (imageProvider === 'openai') {
return { service: 'openai', model: imageSettings.model || 'dall-e-3' };
}
// For Runware provider, use quality-based config
const config = QUALITY_TO_CONFIG[imageQuality];
return { service: config.service, model: config.model };
}, [imageQuality]);
}, [imageQuality, imageProvider, imageSettings.model]);
const availableImageSizes = getImageSizes(
getCurrentImageConfig().service,
getCurrentImageConfig().model
);
// Get the current model's landscape size for display
const currentLandscapeSize = getLandscapeSizeForModel(imageSettings.model || getCurrentImageConfig().model);
// Sectors selection state
const [industries, setIndustries] = useState<Industry[]>([]);
@@ -228,31 +248,15 @@ export default function SiteSettings() {
}
}, [activeTab, siteId]);
// Update image sizes when quality changes
// Update image config when quality changes (all Runware models)
useEffect(() => {
const config = getCurrentImageConfig();
const sizes = getImageSizes(config.service, config.model);
const defaultSize = sizes.length > 0 ? sizes[0].value : '1024x1024';
const validSizes = sizes.map(s => s.value);
const needsFeaturedUpdate = !validSizes.includes(imageSettings.featured_image_size);
if (needsFeaturedUpdate) {
setImageSettings(prev => ({
...prev,
service: config.service,
provider: config.service,
model: config.model,
featured_image_size: needsFeaturedUpdate ? defaultSize : prev.featured_image_size,
}));
} else {
setImageSettings(prev => ({
...prev,
service: config.service,
provider: config.service,
model: config.model,
}));
}
setImageSettings(prev => ({
...prev,
service: config.service,
provider: config.service,
model: config.model,
}));
}, [imageQuality, getCurrentImageConfig]);
// Load sites for selector
@@ -429,21 +433,69 @@ export default function SiteSettings() {
const loadImageSettings = async () => {
try {
setImageSettingsLoading(true);
const imageData = await fetchAPI('/v1/system/settings/integrations/image_generation/');
if (imageData) {
const quality = getQualityFromConfig(imageData.service || imageData.provider, imageData.model);
const response = await fetchAPI('/v1/system/settings/integrations/image_generation/');
// API returns { data: { config: {...} } } structure - try multiple paths
const config = response?.data?.config || response?.config || response?.data || response || {};
console.log('[loadImageSettings] Raw response:', response);
console.log('[loadImageSettings] Extracted config:', config);
if (config) {
// Get provider from config (GlobalIntegrationSettings default_image_service)
const provider = config.provider || config.service || 'runware';
setImageProvider(provider as 'runware' | 'openai');
// Load available styles from backend (provider-specific)
// If not available from API, use default fallbacks
if (config.available_styles && Array.isArray(config.available_styles) && config.available_styles.length > 0) {
setAvailableStyles(config.available_styles);
console.log('[loadImageSettings] Loaded styles from API:', config.available_styles);
} else {
// Fallback styles if API doesn't return them (backward compatibility)
const fallbackStyles = provider === 'openai'
? [
{ value: 'natural', label: 'Natural', description: 'More realistic, photographic style' },
{ value: 'vivid', label: 'Vivid', description: 'Hyper-real, dramatic, artistic' },
]
: [
{ value: 'photorealistic', label: 'Photorealistic', description: 'Ultra realistic photography style, natural lighting, real world look' },
{ value: 'illustration', label: 'Illustration', description: 'Digital illustration, clean lines, artistic but not realistic' },
{ value: '3d_render', label: '3D Render', description: 'Computer generated 3D style, modern, polished, depth and lighting' },
{ value: 'minimal_flat', label: 'Minimal / Flat Design', description: 'Simple shapes, flat colors, modern UI and graphic design look' },
{ value: 'artistic', label: 'Artistic / Painterly', description: 'Expressive, painted or hand drawn aesthetic' },
{ value: 'cartoon', label: 'Cartoon / Stylized', description: 'Playful, exaggerated forms, animated or mascot style' },
];
setAvailableStyles(fallbackStyles);
console.log('[loadImageSettings] Using fallback styles for provider:', provider);
}
// Get model based on provider
let loadedModel = config.model || 'runware:97@1';
if (provider === 'openai') {
loadedModel = config.model || 'dall-e-3';
} else {
loadedModel = config.model || config.runwareModel || 'runware:97@1';
}
const quality = getQualityFromConfig(provider, loadedModel);
setImageQuality(quality);
// Get image_type from config - map old values to new ones
let imageType = config.image_type || (provider === 'openai' ? 'natural' : 'photorealistic');
// Map old style values to new ones
if (imageType === 'realistic') imageType = provider === 'openai' ? 'natural' : 'photorealistic';
setImageSettings({
enabled: imageData.enabled !== false,
service: imageData.service || imageData.provider || 'openai',
provider: imageData.provider || imageData.service || 'openai',
model: imageData.model || 'dall-e-3',
image_type: imageData.image_type || 'realistic',
max_in_article_images: imageData.max_in_article_images || 2,
image_format: imageData.image_format || 'webp',
featured_image_size: imageData.featured_image_size || '1024x1024',
enabled: config.enabled !== false,
service: provider,
provider: provider,
model: loadedModel,
image_type: imageType,
max_in_article_images: config.max_in_article_images || 4,
image_format: config.image_format || 'webp',
});
console.log('[loadImageSettings] Final settings - Provider:', provider, 'Model:', loadedModel, 'Quality:', quality, 'ImageType:', imageType);
}
} catch (error: any) {
console.error('Error loading image settings:', error);
@@ -456,22 +508,28 @@ export default function SiteSettings() {
try {
setImageSettingsSaving(true);
const config = getCurrentImageConfig();
const landscapeSize = getLandscapeSizeForModel(imageSettings.model);
const configToSave = {
enabled: imageSettings.enabled,
service: config.service,
provider: config.service,
model: config.model,
runwareModel: config.service === 'runware' ? config.model : undefined,
service: imageProvider,
provider: imageProvider,
model: imageSettings.model,
runwareModel: imageProvider === 'runware' ? imageSettings.model : undefined,
image_type: imageSettings.image_type,
max_in_article_images: imageSettings.max_in_article_images,
image_format: imageSettings.image_format,
featured_image_size: imageSettings.featured_image_size,
featured_image_size: landscapeSize, // Auto-determined by model
};
await fetchAPI('/v1/system/settings/integrations/image_generation/save/', {
console.log('[saveImageSettings] Saving config:', configToSave);
// URL pattern is /v1/system/settings/integrations/<pk>/save/
const result = await fetchAPI('/v1/system/settings/integrations/image_generation/save/', {
method: 'POST',
body: JSON.stringify(configToSave),
});
console.log('[saveImageSettings] Save result:', result);
toast.success('Image settings saved successfully');
} catch (error: any) {
console.error('Error saving image settings:', error);
@@ -982,55 +1040,111 @@ export default function SiteSettings() {
</div>
) : (
<div className="space-y-6">
{/* Row 1: Image Quality & Style */}
{/* Provider Info Banner - Show which provider is configured */}
<div className={`p-3 rounded-lg flex items-center gap-3 ${
imageProvider === 'openai'
? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800'
: 'bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800'
}`}>
<div className={`p-2 rounded-lg ${
imageProvider === 'openai'
? 'bg-green-100 dark:bg-green-800'
: 'bg-purple-100 dark:bg-purple-800'
}`}>
<ImageIcon className={`w-4 h-4 ${
imageProvider === 'openai'
? 'text-green-600 dark:text-green-400'
: 'text-purple-600 dark:text-purple-400'
}`} />
</div>
<div className="flex-1">
<p className={`text-sm font-medium ${
imageProvider === 'openai'
? 'text-green-700 dark:text-green-300'
: 'text-purple-700 dark:text-purple-300'
}`}>
{imageProvider === 'openai' ? 'OpenAI DALL-E' : 'Runware'} - Image Provider
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{imageProvider === 'openai'
? 'Using DALL-E 3 for high-quality AI images'
: 'Choose from multiple AI models with different quality tiers'}
</p>
</div>
</div>
{/* Row 1: Image Model/Quality & Style */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Label className="mb-2">Image Quality</Label>
<SelectDropdown
options={[
{ value: 'standard', label: 'Standard - Fast & economical (DALL·E 2)' },
{ value: 'premium', label: 'Premium - High quality (DALL·E 3)' },
{ value: 'best', label: 'Best - Highest quality (Runware)' },
]}
value={imageQuality}
onChange={(value) => setImageQuality(value as 'standard' | 'premium' | 'best')}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Higher quality produces better images</p>
<Label className="mb-2">{imageProvider === 'openai' ? 'Image Model' : 'Image Quality'}</Label>
{imageProvider === 'openai' ? (
<>
<SelectDropdown
options={DALLE_MODEL_CHOICES.map(m => ({
value: m.value,
label: m.label,
}))}
value={imageSettings.model}
onChange={(value) => setImageSettings({ ...imageSettings, model: value })}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">DALL-E 3 generates high-quality images (40 credits/image)</p>
</>
) : (
<>
<SelectDropdown
options={RUNWARE_MODEL_CHOICES.map(m => ({
value: m.value === 'runware:97@1' ? 'basic' : m.value === 'bria:10@1' ? 'quality' : 'premium',
label: m.label,
}))}
value={imageQuality}
onChange={(value) => {
setImageQuality(value as 'basic' | 'quality' | 'premium');
const model = value === 'basic' ? 'runware:97@1' : value === 'quality' ? 'bria:10@1' : 'google:4@2';
setImageSettings({ ...imageSettings, model });
}}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Higher credit cost = better image quality</p>
</>
)}
</div>
<div>
<Label className="mb-2">Image Style</Label>
<SelectDropdown
options={[
{ value: 'realistic', label: 'Realistic' },
{ value: 'artistic', label: 'Artistic' },
{ value: 'cartoon', label: 'Cartoon' },
]}
options={availableStyles.map(style => ({
value: style.value,
label: style.label,
}))}
value={imageSettings.image_type}
onChange={(value) => setImageSettings({ ...imageSettings, image_type: value as any })}
onChange={(value) => setImageSettings({ ...imageSettings, image_type: value })}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Choose the visual style that matches your brand</p>
{/* Show description of selected style */}
<p className="text-xs text-gray-500 mt-1">
{availableStyles.find(s => s.value === imageSettings.image_type)?.description || 'Choose the visual style that matches your brand'}
</p>
</div>
</div>
{/* Row 2: Featured Image Size */}
<div>
{/* Row 2: Featured Image Info Card (compact, not full width) */}
<div className="max-w-md">
<Label className="mb-2">Featured Image</Label>
<div className="p-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gradient-to-r from-purple-500 to-brand-500 text-white">
<div className="flex items-center justify-between mb-3">
<div className="font-medium">Featured Image Size</div>
<div className="text-xs bg-white/20 px-2 py-1 rounded">Landscape (Model-specific)</div>
<div className="p-4 rounded-lg bg-gradient-to-r from-brand-500 to-purple-500 text-white">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-white/20 flex items-center justify-center">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<div>
<div className="font-medium">Always Landscape</div>
<div className="text-sm text-white/80">{currentLandscapeSize} pixels</div>
</div>
</div>
<SelectDropdown
options={availableImageSizes}
value={imageSettings.featured_image_size}
onChange={(value) => setImageSettings({ ...imageSettings, featured_image_size: value })}
className="w-full [&_.igny8-select-styled]:bg-white/10 [&_.igny8-select-styled]:border-white/20 [&_.igny8-select-styled]:text-white"
/>
<p className="text-xs text-white/70 mt-2">
In-article images alternate: Square (1024×1024) Landscape Square Landscape
<p className="text-xs text-white/70 mt-3">
Featured image size is automatically set based on your selected model
</p>
</div>
</div>
@@ -1051,7 +1165,7 @@ export default function SiteSettings() {
className="w-full"
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Images 1 & 3: Square | Images 2 & 4: Landscape
Images 1 & 3: Square (1024×1024) | Images 2 & 4: Landscape
</p>
</div>

View File

@@ -326,12 +326,14 @@ export default function Images() {
.sort((a, b) => (a.position || 0) - (b.position || 0));
pendingInArticle.forEach((img, idx) => {
// Position is 0-indexed in backend, but labels should be 1-indexed for users
const displayPosition = (img.position ?? idx) + 1;
queue.push({
imageId: img.id || null,
index: queueIndex++,
label: `In-Article Image ${img.position || idx + 1}`,
label: `In-Article Image ${displayPosition}`,
type: 'in_article',
position: img.position || idx + 1,
position: img.position ?? idx,
contentTitle: contentImages.content_title || `Content #${contentId}`,
prompt: img.prompt || undefined,
status: 'pending',