Files
igny8/frontend/src/components/common/ImageGenerationCard.tsx
IGNY8 VPS (Salman) 0526553c9b Phase 1: Code cleanup - remove unused pages, components, and console.logs
- Deleted 6 empty folders (pages/Admin, pages/admin, pages/settings, components/debug, components/widgets, components/metrics)
- Removed unused template components:
  - ecommerce/ (7 files)
  - sample-componeents/ (2 HTML files)
  - charts/bar/ and charts/line/
  - tables/BasicTables/
- Deleted deprecated file: CurrentProcessingCard.old.tsx
- Removed console.log statements from:
  - UserProfile components (UserMetaCard, UserAddressCard, UserInfoCard)
  - Automation/ConfigModal
  - ImageQueueModal (8 statements)
  - ImageGenerationCard (7 statements)
- Applied ESLint auto-fixes (9 errors fixed)
- All builds pass ✓
- TypeScript compiles without errors ✓
2026-01-09 15:22:23 +00:00

422 lines
14 KiB
TypeScript

import { ReactNode, useState, useEffect } from 'react';
import Button from '../ui/button/Button';
import TextArea from '../form/input/TextArea';
import Select from '../form/Select';
import Label from '../form/Label';
import { useToast } from '../ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
interface ImageGenerationCardProps {
title: string;
description?: string;
integrationId: string;
icon?: ReactNode;
}
interface GeneratedImage {
url: string;
revised_prompt?: string;
model?: string;
provider?: string;
size?: string;
format?: string;
cost?: string;
}
interface ImageSettings {
service?: string;
model?: string;
runwareModel?: string;
}
/**
* Image Generation Testing Card Component
* Full implementation with form fields and image display
*/
export default function ImageGenerationCard({
title,
description,
integrationId,
icon,
}: ImageGenerationCardProps) {
const toast = useToast();
const [isGenerating, setIsGenerating] = useState(false);
const [prompt, setPrompt] = useState('');
const [negativePrompt, setNegativePrompt] = useState('text, watermark, logo, overlay, title, caption, writing on walls, writing on objects, UI, infographic elements, post title');
const [imageType, setImageType] = useState('realistic');
const [imageSize, setImageSize] = useState('1024x1024');
const [imageFormat, setImageFormat] = useState('webp');
const [imageSettings, setImageSettings] = useState<ImageSettings>({});
// Valid image sizes per model (from OpenAI official documentation)
const VALID_SIZES_BY_MODEL: Record<string, string[]> = {
'dall-e-3': ['1024x1024', '1024x1792', '1792x1024'],
'dall-e-2': ['256x256', '512x512', '1024x1024'],
};
// Get valid sizes for current model
const getValidSizes = (): string[] => {
const service = imageSettings.service || 'openai';
const model = service === 'openai'
? (imageSettings.model || 'dall-e-3')
: null;
if (model && VALID_SIZES_BY_MODEL[model]) {
return VALID_SIZES_BY_MODEL[model];
}
// Default to DALL-E 3 sizes if unknown
return VALID_SIZES_BY_MODEL['dall-e-3'];
};
// Update size if current size is invalid for the selected model
useEffect(() => {
const validSizes = getValidSizes();
if (validSizes.length > 0 && !validSizes.includes(imageSize)) {
// Reset to first valid size (usually the default)
setImageSize(validSizes[0]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [imageSettings.model, imageSettings.service]); // imageSize intentionally omitted to avoid infinite loop
const [generatedImage, setGeneratedImage] = useState<GeneratedImage | null>(null);
const [error, setError] = useState<string | null>(null);
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
// Load default image generation settings on mount
useEffect(() => {
const loadImageSettings = async () => {
try {
const response = await fetch(
`${API_BASE_URL}/v1/system/settings/integrations/image_generation/`,
{ credentials: 'include' }
);
if (response.ok) {
const data = await response.json();
if (data.success && data.data) {
setImageSettings(data.data);
}
}
} catch (error) {
console.error('Error loading image settings:', error);
}
};
loadImageSettings();
}, [API_BASE_URL]);
const handleGenerate = async () => {
if (!prompt.trim()) {
toast.error('Please enter a prompt description');
return;
}
setIsGenerating(true);
setError(null);
setGeneratedImage(null);
try {
// Get the default service and model from settings
const service = imageSettings.service || 'openai';
const model = service === 'openai'
? (imageSettings.model || 'dall-e-3')
: (imageSettings.runwareModel || 'runware:97@1');
// Build prompt with template (similar to reference plugin)
const fullPrompt = `Create a high-quality ${imageType} image. ${prompt}`;
const requestBody = {
prompt: fullPrompt,
negative_prompt: negativePrompt,
image_type: imageType,
image_size: imageSize,
image_format: imageFormat,
provider: service,
model: model,
};
// fetchAPI extracts data from unified format {success: true, data: {...}}
// So data is the extracted response payload
const data = await fetchAPI('/v1/system/settings/integrations/image_generation/generate/', {
method: 'POST',
body: JSON.stringify(requestBody),
});
// fetchAPI extracts data from unified format, so data is the response payload
// If fetchAPI didn't throw, the request was successful
if (!data || typeof data !== 'object') {
throw new Error('Invalid response format');
}
const imageData = {
url: data.image_url,
revised_prompt: data.revised_prompt,
model: data.model || model,
provider: data.provider || service,
size: imageSize,
format: imageFormat.toUpperCase(),
cost: data.cost,
};
setGeneratedImage(imageData);
// Emit custom event for ImageResultCard to listen to
window.dispatchEvent(
new CustomEvent('imageGenerated', {
detail: imageData,
})
);
toast.success('Image generated successfully!');
} catch (err: any) {
console.error('[ImageGenerationCard] Error in handleGenerate:', {
error: err,
message: err.message,
stack: err.stack,
});
const errorMessage = err.message || 'Failed to generate image';
setError(errorMessage);
// Emit error event for ImageResultCard
window.dispatchEvent(
new CustomEvent('imageGenerationError', {
detail: errorMessage,
})
);
toast.error(errorMessage);
} finally {
console.log('[ImageGenerationCard] handleGenerate completed');
setIsGenerating(false);
}
};
// Get display name for provider and model
const getProviderDisplay = () => {
const service = imageSettings.service || 'openai';
if (service === 'openai') {
const model = imageSettings.model || '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 {
return 'Runware';
}
};
// Image size options - dynamically generated based on selected model
const sizeLabels: Record<string, string> = {
'1024x1024': 'Square - 1024 x 1024',
'1024x1792': 'Portrait - 1024 x 1792',
'1792x1024': 'Landscape - 1792 x 1024',
'256x256': 'Small - 256 x 256',
'512x512': 'Medium - 512 x 512',
};
const sizeOptions = getValidSizes().map(size => ({
value: size,
label: sizeLabels[size] || size,
}));
// Image type options
const typeOptions = [
{ value: 'realistic', label: 'Realistic' },
{ value: 'illustration', label: 'Illustration' },
{ value: '3D render', label: '3D Render' },
{ value: 'minimalist', label: 'Minimalist' },
{ value: 'cartoon', label: 'Cartoon' },
];
// Format options
const formatOptions = [
{ value: 'webp', label: 'WEBP' },
{ value: 'jpg', label: 'JPG' },
{ value: 'png', label: 'PNG' },
];
return (
<article className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/3">
<div className="relative p-5 pb-6">
{icon && (
<div className="mb-4 inline-flex h-10 w-10 items-center justify-center">
{icon}
</div>
)}
<h3 className="mb-2 text-base font-semibold text-gray-800 dark:text-white/90">
{title}
</h3>
{description && (
<p className="text-sm text-gray-500 dark:text-gray-400">
{description}
</p>
)}
</div>
<div className="border-t border-gray-200 p-5 dark:border-gray-800">
<div className="space-y-5">
{/* API Provider and Model Display */}
<div className="flex items-center gap-3 rounded-lg bg-brand-50 px-4 py-3 dark:bg-brand-900/20">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
className="text-brand-600 dark:text-brand-400"
>
<path
d="M10 2L3 7V17C3 17.5304 3.21071 18.0391 3.58579 18.4142C3.96086 18.7893 4.46957 19 5 19H15C15.5304 19 16.0391 18.7893 16.4142 18.4142C16.7893 18.0391 17 17.5304 17 17V7L10 2Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<div>
<p className="text-xs font-medium text-brand-600 dark:text-brand-400">Provider & Model</p>
<p className="text-sm font-semibold text-brand-900 dark:text-brand-200">
{getProviderDisplay()}
</p>
</div>
</div>
{/* Prompt Description - Full Width */}
<div>
<Label className="mb-2">
Prompt Description *
</Label>
<TextArea
value={prompt}
onChange={(val) => setPrompt(val)}
rows={6}
placeholder="Describe the visual elements, style, mood, and composition you want in the image..."
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Describe the visual elements, style, mood, and composition you want in the image.
</p>
</div>
{/* Negative Prompt - Small */}
<div>
<Label className="mb-2">
Negative Prompt
</Label>
<TextArea
value={negativePrompt}
onChange={(val) => setNegativePrompt(val)}
rows={2}
placeholder="Describe what you DON'T want in the image..."
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Specify elements to avoid in the generated image (text, watermarks, logos, etc.).
</p>
</div>
{/* 3 Column Dropdowns */}
<div className="grid grid-cols-3 gap-4">
{/* Image Type */}
<div>
<Label className="mb-2">
Image Type
</Label>
<Select
options={typeOptions}
defaultValue={imageType}
onChange={(val) => setImageType(val)}
/>
</div>
{/* Image Size */}
<div>
<Label className="mb-2">
Image Size
</Label>
<Select
options={sizeOptions}
defaultValue={imageSize}
onChange={(val) => setImageSize(val)}
/>
</div>
{/* Image Format */}
<div>
<Label className="mb-2">
Image Format
</Label>
<Select
options={formatOptions}
defaultValue={imageFormat}
onChange={(val) => setImageFormat(val)}
/>
</div>
</div>
{/* Generate Button - Bottom Right */}
<div className="flex justify-end">
<Button
onClick={handleGenerate}
disabled={isGenerating || !prompt.trim()}
variant="primary"
size="md"
>
{isGenerating ? (
<>
<svg
className="h-4 w-4 animate-spin mr-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Generating...
</>
) : (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="3" width="18" height="18" rx="2" />
<circle cx="9" cy="9" r="2" />
<path d="M21 15l-3.086-3.086a2 2 0 00-2.828 0L6 21" />
</svg>
Generate Image
</>
)}
</Button>
</div>
</div>
</div>
{/* Error display */}
{error && (
<div className="mt-4 rounded-lg border border-error-200 bg-error-50 p-4 dark:border-error-800 dark:bg-error-900/20">
<p className="text-sm text-error-600 dark:text-error-400">{error}</p>
</div>
)}
</article>
);
}