- 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 ✓
422 lines
14 KiB
TypeScript
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>
|
|
);
|
|
}
|