AI Analysis and Content Strategy fixes. Enhanced Strategy Routes refactoring.
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
import React, { useEffect, useMemo, useState, forwardRef, useImperativeHandle } from 'react';
|
||||
import { Box, Button, MenuItem, Select, TextField, Typography, FormControl, InputLabel, Grid, Card, CardMedia, CircularProgress, LinearProgress, Collapse, IconButton, Tabs, Tab, Tooltip } from '@mui/material';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import {
|
||||
Box, Button, MenuItem, Select, TextField, Typography, FormControl, InputLabel, Grid,
|
||||
Card, CardMedia, CircularProgress, LinearProgress, Tabs, Tab,
|
||||
Tooltip, Alert, Chip, IconButton
|
||||
} from '@mui/material';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { useImageGeneration, ImageGenerationRequest, fetchPromptSuggestions } from './useImageGeneration';
|
||||
|
||||
type Provider = 'gemini' | 'huggingface' | 'stability';
|
||||
type Provider = 'huggingface' | 'stability' | 'wavespeed';
|
||||
type ImageType = 'realistic' | 'chart' | 'conceptual' | 'diagram' | 'illustration' | 'background';
|
||||
|
||||
interface ImageGeneratorProps {
|
||||
defaultProvider?: Provider;
|
||||
@@ -30,60 +35,181 @@ interface ImageGeneratorProps {
|
||||
export interface ImageGeneratorHandle {
|
||||
suggest: () => Promise<void> | void;
|
||||
generate: () => Promise<void> | void;
|
||||
openAdvanced: () => void;
|
||||
closeAdvanced: () => void;
|
||||
}
|
||||
|
||||
export const ImageGenerator = React.forwardRef<ImageGeneratorHandle, ImageGeneratorProps>((
|
||||
{ defaultProvider, defaultModel, defaultPrompt, onImageReady, context },
|
||||
ref
|
||||
) => {
|
||||
const [provider, setProvider] = useState<Provider>(defaultProvider || (process.env.NEXT_PUBLIC_GPT_PROVIDER as Provider) || 'huggingface');
|
||||
const [model, setModel] = useState<string>(defaultModel || 'black-forest-labs/FLUX.1-Krea-dev');
|
||||
// Default to wavespeed for cost-effective blog images
|
||||
const initialProvider = defaultProvider || 'wavespeed';
|
||||
const [provider, setProvider] = useState<Provider>(initialProvider);
|
||||
|
||||
// Initialize model based on the actual provider
|
||||
const getDefaultModelForProvider = (prov: Provider): string => {
|
||||
if (prov === 'wavespeed') return 'qwen-image';
|
||||
if (prov === 'huggingface') return 'black-forest-labs/FLUX.1-Krea-dev';
|
||||
if (prov === 'stability') return 'stable-diffusion-xl-1024-v1-0';
|
||||
return '';
|
||||
};
|
||||
|
||||
const getAvailableModelsForProvider = (prov: Provider): string[] => {
|
||||
if (prov === 'wavespeed') return ['qwen-image', 'ideogram-v3-turbo', 'flux-kontext-pro'];
|
||||
if (prov === 'huggingface') return ['black-forest-labs/FLUX.1-Krea-dev', 'black-forest-labs/FLUX.1-dev', 'runwayml/flux-dev'];
|
||||
if (prov === 'stability') return ['stable-diffusion-xl-1024-v1-0', 'stable-diffusion-xl-base-1.0'];
|
||||
return [];
|
||||
};
|
||||
|
||||
// Get max dimensions for a model
|
||||
const getMaxDimensionsForModel = (modelName: string): { maxWidth: number; maxHeight: number } => {
|
||||
const modelLower = modelName.toLowerCase();
|
||||
// Wavespeed models have 1024x1024 max
|
||||
if (modelLower === 'qwen-image' || modelLower === 'ideogram-v3-turbo' || modelLower === 'flux-kontext-pro') {
|
||||
return { maxWidth: 1024, maxHeight: 1024 };
|
||||
}
|
||||
// HuggingFace and Stability models typically support higher resolutions
|
||||
return { maxWidth: 2048, maxHeight: 2048 };
|
||||
};
|
||||
|
||||
// Get model-specific tips and warnings
|
||||
const getModelGuidance = (modelName: string, imgType: ImageType): { tips: string[]; warnings: string[]; recommendations: string } => {
|
||||
const modelLower = modelName.toLowerCase();
|
||||
const tips: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
let recommendations = '';
|
||||
|
||||
if (modelLower === 'ideogram-v3-turbo') {
|
||||
tips.push('Best for images with simple text overlays (3-5 words max)');
|
||||
tips.push('Excellent photorealistic quality');
|
||||
tips.push('Superior text rendering compared to other models');
|
||||
if (imgType === 'chart' || imgType === 'diagram') {
|
||||
warnings.push('Avoid complex infographics - use simple charts with designated text overlay areas');
|
||||
recommendations = 'Create clean backgrounds with high-contrast zones for text placement, not embedded text';
|
||||
}
|
||||
if (imgType === 'conceptual' || imgType === 'background') {
|
||||
recommendations = 'Design with text overlay zones in mind (top 20% or bottom 20% of image)';
|
||||
}
|
||||
} else if (modelLower === 'qwen-image') {
|
||||
tips.push('Fast and cost-effective generation');
|
||||
tips.push('Best for abstract concepts and simple compositions');
|
||||
warnings.push('⚠️ Does NOT render readable text well - design for text overlay areas only');
|
||||
warnings.push('Avoid requesting text, words, or labels in the image itself');
|
||||
if (imgType === 'chart' || imgType === 'diagram') {
|
||||
warnings.push('Use abstract representations of data, not actual charts with text');
|
||||
recommendations = 'Create visual metaphors and patterns that represent data concepts';
|
||||
}
|
||||
recommendations = 'Design clean backgrounds with space for text overlays (never embed text)';
|
||||
} else if (modelLower === 'flux-kontext-pro') {
|
||||
tips.push('Excellent typography and text rendering capabilities');
|
||||
tips.push('Improved prompt adherence for consistent results');
|
||||
tips.push('Best for images with text elements, typography, and professional designs');
|
||||
tips.push('Cost-effective at $0.04 per image');
|
||||
if (imgType === 'chart' || imgType === 'diagram') {
|
||||
tips.push('Can render simple charts with text labels effectively');
|
||||
recommendations = 'Use for data visualizations that require clear text labels and typography';
|
||||
} else if (imgType === 'realistic' || imgType === 'illustration') {
|
||||
recommendations = 'Great for professional designs with text overlays or embedded typography';
|
||||
} else {
|
||||
recommendations = 'Ideal for blog images that need clear, readable text elements';
|
||||
}
|
||||
}
|
||||
|
||||
// Image type specific warnings
|
||||
if (imgType === 'chart') {
|
||||
warnings.push('Complex infographics are too difficult for current AI models');
|
||||
recommendations = 'Use simple visual representations with designated text overlay areas';
|
||||
}
|
||||
|
||||
return { tips, warnings, recommendations };
|
||||
};
|
||||
|
||||
// Initialize model - ensure it's valid for the initial provider
|
||||
const initialModel = defaultModel || getDefaultModelForProvider(initialProvider);
|
||||
const [model, setModel] = useState<string>(initialModel);
|
||||
const [imageType, setImageType] = useState<ImageType>('conceptual');
|
||||
const [prompt, setPrompt] = useState<string>(defaultPrompt || '');
|
||||
const [negative, setNegative] = useState<string>('');
|
||||
const [width, setWidth] = useState<number>(1024);
|
||||
const [height, setHeight] = useState<number>(1024);
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const { isGenerating, error, result, generate } = useImageGeneration();
|
||||
const [loadingSuggestions, setLoadingSuggestions] = useState(false);
|
||||
const [suggestions, setSuggestions] = useState<Array<{ prompt: string; negative_prompt?: string; width?: number; height?: number; overlay_text?: string }>>([]);
|
||||
const [suggestionIndex, setSuggestionIndex] = useState<number>(0);
|
||||
|
||||
const canGenerate = useMemo(() => prompt.trim().length > 0 && !isGenerating, [prompt, isGenerating]);
|
||||
const canOptimize = useMemo(() => prompt.trim().length > 0 && !loadingSuggestions, [prompt, loadingSuggestions]);
|
||||
|
||||
// High-contrast input styling for readability on light backgrounds
|
||||
// Sync model when provider changes - ensure model is always valid for current provider
|
||||
useEffect(() => {
|
||||
const availableModels = getAvailableModelsForProvider(provider);
|
||||
// Check if current model is valid for the new provider
|
||||
if (!availableModels.includes(model)) {
|
||||
// Model is not valid for this provider, set to default
|
||||
const defaultModelForProvider = getDefaultModelForProvider(provider);
|
||||
if (defaultModelForProvider) {
|
||||
setModel(defaultModelForProvider);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [provider]); // Only depend on provider to avoid loops
|
||||
|
||||
// Clamp dimensions when model changes to ensure they don't exceed model limits
|
||||
useEffect(() => {
|
||||
const { maxWidth, maxHeight } = getMaxDimensionsForModel(model);
|
||||
if (width > maxWidth) {
|
||||
setWidth(maxWidth);
|
||||
}
|
||||
if (height > maxHeight) {
|
||||
setHeight(maxHeight);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [model]); // Only depend on model to avoid loops
|
||||
|
||||
// Get current model guidance for display
|
||||
const modelGuidance = useMemo(() => getModelGuidance(model, imageType), [model, imageType]);
|
||||
|
||||
// Professional styling with improved contrast and readability
|
||||
const textInputSx = {
|
||||
'& .MuiInputBase-input': { color: '#202124' },
|
||||
'& .MuiInputLabel-root': { color: '#5f6368' },
|
||||
'& .MuiOutlinedInput-notchedOutline': { borderColor: '#cbd5e1' },
|
||||
'&:hover .MuiOutlinedInput-notchedOutline': { borderColor: '#94a3b8' },
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': { borderColor: '#1976d2' },
|
||||
backgroundColor: '#ffffff'
|
||||
'& .MuiInputBase-input': {
|
||||
color: '#1a1a1a',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5'
|
||||
},
|
||||
'& .MuiInputLabel-root': {
|
||||
color: '#5f6368',
|
||||
fontSize: '14px',
|
||||
fontWeight: 500
|
||||
},
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#dadce0',
|
||||
borderWidth: '1.5px'
|
||||
},
|
||||
'&:hover .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#80868b'
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#1976d2',
|
||||
borderWidth: '2px'
|
||||
},
|
||||
backgroundColor: '#ffffff',
|
||||
'& .MuiFormHelperText-root': {
|
||||
fontSize: '12px',
|
||||
color: '#5f6368',
|
||||
marginTop: '4px'
|
||||
}
|
||||
} as const;
|
||||
|
||||
// Default negative prompts by provider for blog writer use-case
|
||||
useEffect(() => {
|
||||
if (negative.trim().length > 0) return;
|
||||
if (provider === 'huggingface') {
|
||||
if (provider === 'wavespeed') {
|
||||
setNegative('people posing, social media graphics, posters, text rendered as images, busy compositions, watermarks, brand logos, random people, cartoon, low quality, blurry, distorted');
|
||||
} else if (provider === 'huggingface') {
|
||||
setNegative('blurry, distorted, cartoon, low quality, bad anatomy, extra limbs, watermark, brand logos, text artifacts, oversaturated, noisy, jpeg artifacts');
|
||||
} else if (provider === 'gemini') {
|
||||
setNegative('cartoon, clip-art, abstract, noisy, low resolution, artifacts, watermark, brand logos, text artifacts');
|
||||
} else {
|
||||
setNegative('blurry, distorted, low quality, bad anatomy, extra limbs, watermark, brand logos, jpeg artifacts, oversharpened, text artifacts');
|
||||
}
|
||||
// run once on mount (and when provider changes if negative is empty)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [provider]);
|
||||
|
||||
// Auto-suggest on open for better defaults (only if no initial prompt)
|
||||
useEffect(() => {
|
||||
if (!prompt || prompt.trim().length === 0) {
|
||||
// fire and forget; UI shows spinner on the button if user clicks again
|
||||
suggestPrompt().catch(() => {});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [provider, negative]);
|
||||
|
||||
// Provider-specialized prompt suggestions using backend structured response; fallback locally
|
||||
const suggestPrompt = async () => {
|
||||
@@ -91,6 +217,8 @@ export const ImageGenerator = React.forwardRef<ImageGeneratorHandle, ImageGenera
|
||||
try {
|
||||
const payload = {
|
||||
provider,
|
||||
model,
|
||||
image_type: imageType,
|
||||
title: context?.title || context?.section?.heading || defaultPrompt || '',
|
||||
section: context?.section || undefined,
|
||||
research: context?.research || undefined,
|
||||
@@ -130,6 +258,13 @@ export const ImageGenerator = React.forwardRef<ImageGeneratorHandle, ImageGenera
|
||||
};
|
||||
|
||||
const onGenerate = async () => {
|
||||
// Validate dimensions against model limits
|
||||
const { maxWidth, maxHeight } = getMaxDimensionsForModel(model);
|
||||
if (width > maxWidth || height > maxHeight) {
|
||||
alert(`Resolution ${width}x${height} exceeds maximum ${maxWidth}x${maxHeight} for model ${model}. Please adjust the dimensions.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const req: ImageGenerationRequest = { prompt, negative_prompt: negative, provider, model, width, height };
|
||||
const res = await generate(req);
|
||||
if (res && onImageReady) onImageReady(res.image_base64);
|
||||
@@ -142,154 +277,634 @@ export const ImageGenerator = React.forwardRef<ImageGeneratorHandle, ImageGenera
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
suggest: () => suggestPrompt(),
|
||||
generate: () => onGenerate(),
|
||||
openAdvanced: () => setShowAdvanced(v => !v),
|
||||
closeAdvanced: () => setShowAdvanced(false)
|
||||
generate: () => onGenerate()
|
||||
}));
|
||||
|
||||
// Get cost info for display
|
||||
const getCostInfo = () => {
|
||||
if (provider === 'wavespeed') {
|
||||
if (model === 'qwen-image') return { cost: '$0.05', description: 'Fast generation, optimized for blog content' };
|
||||
if (model === 'ideogram-v3-turbo') return { cost: '$0.10', description: 'Superior text rendering, photorealistic' };
|
||||
if (model === 'flux-kontext-pro') return { cost: '$0.04', description: 'Professional typography, improved prompt adherence' };
|
||||
return { cost: '$0.05', description: 'Cost-effective blog images' };
|
||||
}
|
||||
if (provider === 'huggingface') {
|
||||
return { cost: '~$0.08', description: 'Photorealistic Flux models' };
|
||||
}
|
||||
if (provider === 'stability') {
|
||||
return { cost: '$0.04', description: 'SDXL-quality professional outputs' };
|
||||
}
|
||||
return { cost: 'Varies', description: 'Check provider pricing' };
|
||||
};
|
||||
|
||||
const costInfo = getCostInfo();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600, color: '#202124' }}>Generate Blog Section Image</Typography>
|
||||
|
||||
{/* Advanced Options in Header Area */}
|
||||
<Collapse in={showAdvanced}>
|
||||
<Box sx={{ mb: 2, border: '1px solid #e0e0e0', borderRadius: 1, p: 1.5, backgroundColor: '#fafafa', color: '#202124' }}>
|
||||
<Box sx={{
|
||||
maxWidth: '900px',
|
||||
mx: 'auto',
|
||||
p: 3,
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '8px'
|
||||
}}>
|
||||
{/* Removed header - title is in modal header */}
|
||||
|
||||
{/* Cost Information Alert */}
|
||||
{provider === 'wavespeed' && (
|
||||
<Alert
|
||||
severity="info"
|
||||
icon={<InfoIcon />}
|
||||
sx={{
|
||||
mb: 2,
|
||||
backgroundColor: '#e3f2fd',
|
||||
'& .MuiAlert-icon': { color: '#1976d2' },
|
||||
'& .MuiAlert-message': { color: '#1565c0' }
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500, mb: 0.5 }}>
|
||||
💰 WaveSpeed Pricing (Cost-Effective for Blog Images)
|
||||
</Typography>
|
||||
<Grid container spacing={1} sx={{ mt: 0.5 }}>
|
||||
<Grid item xs={4}>
|
||||
<Typography variant="body2" sx={{ fontSize: '13px' }}>
|
||||
<strong>Qwen Image:</strong> $0.05/image
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ display: 'block', color: '#5f6368' }}>
|
||||
Fast generation, optimized for blog content
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Typography variant="body2" sx={{ fontSize: '13px' }}>
|
||||
<strong>Ideogram V3 Turbo:</strong> $0.10/image
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ display: 'block', color: '#5f6368' }}>
|
||||
Superior text rendering, photorealistic
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Typography variant="body2" sx={{ fontSize: '13px' }}>
|
||||
<strong>FLUX Kontext Pro:</strong> $0.04/image
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ display: 'block', color: '#5f6368' }}>
|
||||
Professional typography, improved prompt adherence
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Advanced Options - Always Visible */}
|
||||
<Box sx={{
|
||||
mb: 2,
|
||||
p: 2,
|
||||
border: '1.5px solid #e8eaed',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: '#f8f9fa'
|
||||
}}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={3}>
|
||||
<Tooltip title="Select the AI image generation provider. Hugging Face offers photorealistic Flux models, Gemini provides brand-safe editorial images, and Stability AI delivers SDXL-quality professional outputs." placement="top" arrow>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Provider</InputLabel>
|
||||
<Select value={provider} label="Provider" onChange={(e) => setProvider(e.target.value as Provider)} sx={textInputSx} MenuProps={{ PaperProps: { sx: { color: '#202124' } } }}>
|
||||
<MenuItem value="huggingface">Hugging Face</MenuItem>
|
||||
<MenuItem value="gemini">Gemini</MenuItem>
|
||||
<MenuItem value="stability">Stability</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Tooltip>
|
||||
<Grid item xs={12} md={4}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="provider-select-label" sx={{ fontSize: '14px' }}>Provider</InputLabel>
|
||||
<Select
|
||||
labelId="provider-select-label"
|
||||
value={provider}
|
||||
label="Provider"
|
||||
onChange={(e) => {
|
||||
const newProvider = e.target.value as Provider;
|
||||
setProvider(newProvider);
|
||||
setModel(getDefaultModelForProvider(newProvider));
|
||||
}}
|
||||
sx={{
|
||||
...textInputSx,
|
||||
'& .MuiSelect-select': {
|
||||
cursor: 'pointer'
|
||||
}
|
||||
}}
|
||||
MenuProps={{
|
||||
disablePortal: true,
|
||||
PaperProps: {
|
||||
sx: {
|
||||
zIndex: 2200,
|
||||
color: '#202124',
|
||||
maxHeight: 300,
|
||||
'& .MuiMenuItem-root': {
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f5f5f5'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
anchorOrigin: {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
},
|
||||
transformOrigin: {
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value="wavespeed">WaveSpeed AI</MenuItem>
|
||||
<MenuItem value="huggingface">Hugging Face</MenuItem>
|
||||
<MenuItem value="stability">Stability AI</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={5}>
|
||||
<Tooltip title="Specify the exact model to use. Leave empty to use the provider's default. For Hugging Face, the default is FLUX.1-Krea-dev, optimized for photorealistic blog images." placement="top" arrow>
|
||||
<TextField fullWidth label="Model" value={model} onChange={(e) => setModel(e.target.value)} helperText={provider === 'huggingface' ? 'Default: black-forest-labs/FLUX.1-Krea-dev' : 'Leave empty to use provider default'} sx={textInputSx} />
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="model-select-label" sx={{ fontSize: '14px' }}>Model</InputLabel>
|
||||
<Select
|
||||
labelId="model-select-label"
|
||||
value={model}
|
||||
label="Model"
|
||||
onChange={(e) => setModel(e.target.value)}
|
||||
sx={{
|
||||
...textInputSx,
|
||||
'& .MuiSelect-select': {
|
||||
cursor: 'pointer'
|
||||
}
|
||||
}}
|
||||
MenuProps={{
|
||||
disablePortal: true,
|
||||
PaperProps: {
|
||||
sx: {
|
||||
zIndex: 2200,
|
||||
color: '#202124',
|
||||
maxHeight: 300,
|
||||
'& .MuiMenuItem-root': {
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f5f5f5'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
anchorOrigin: {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
},
|
||||
transformOrigin: {
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}
|
||||
}}
|
||||
>
|
||||
{getAvailableModelsForProvider(provider).map((m) => (
|
||||
<MenuItem key={m} value={m}>{m}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<Typography variant="caption" sx={{ mt: 0.5, display: 'block', color: '#5f6368', fontSize: '12px' }}>
|
||||
{provider === 'wavespeed'
|
||||
? 'qwen-image ($0.05), ideogram-v3-turbo ($0.10), or flux-kontext-pro ($0.04)'
|
||||
: provider === 'huggingface'
|
||||
? 'Default: black-forest-labs/FLUX.1-Krea-dev'
|
||||
: 'Default: stable-diffusion-xl-1024-v1-0'}
|
||||
</Typography>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={3}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="image-type-select-label" sx={{ fontSize: '14px' }}>Image Type</InputLabel>
|
||||
<Select
|
||||
labelId="image-type-select-label"
|
||||
value={imageType}
|
||||
label="Image Type"
|
||||
onChange={(e) => setImageType(e.target.value as ImageType)}
|
||||
sx={{
|
||||
...textInputSx,
|
||||
'& .MuiSelect-select': {
|
||||
cursor: 'pointer'
|
||||
}
|
||||
}}
|
||||
MenuProps={{
|
||||
disablePortal: true,
|
||||
PaperProps: {
|
||||
sx: {
|
||||
zIndex: 2200,
|
||||
color: '#202124',
|
||||
maxHeight: 300,
|
||||
'& .MuiMenuItem-root': {
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f5f5f5'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value="realistic">Realistic (Photography)</MenuItem>
|
||||
<MenuItem value="chart">Chart/Data Visualization</MenuItem>
|
||||
<MenuItem value="conceptual">Conceptual (Abstract)</MenuItem>
|
||||
<MenuItem value="diagram">Diagram (Technical)</MenuItem>
|
||||
<MenuItem value="illustration">Illustration (Stylized)</MenuItem>
|
||||
<MenuItem value="background">Background (Text Overlay)</MenuItem>
|
||||
</Select>
|
||||
<Typography variant="caption" sx={{ mt: 0.5, display: 'block', color: '#5f6368', fontSize: '12px' }}>
|
||||
Select the type of image you want to generate
|
||||
</Typography>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={6} md={1.5}>
|
||||
<Tooltip
|
||||
title={`Image width in pixels. Max for ${model}: ${getMaxDimensionsForModel(model).maxWidth}px. Recommended: 1024 for square images, 1920 for landscape covers.`}
|
||||
placement="top"
|
||||
arrow
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
label="Width"
|
||||
value={width}
|
||||
onChange={(e) => {
|
||||
const newWidth = parseInt(e.target.value || '0', 10);
|
||||
const { maxWidth } = getMaxDimensionsForModel(model);
|
||||
setWidth(Math.min(newWidth, maxWidth));
|
||||
}}
|
||||
inputProps={{ min: 64, max: getMaxDimensionsForModel(model).maxWidth }}
|
||||
sx={textInputSx}
|
||||
error={width > getMaxDimensionsForModel(model).maxWidth}
|
||||
helperText={width > getMaxDimensionsForModel(model).maxWidth ? `Max: ${getMaxDimensionsForModel(model).maxWidth}px` : ''}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item xs={6} md={2}>
|
||||
<Tooltip title="Image width in pixels. Recommended: 1024 for square images, 1920 for landscape covers. Higher values increase quality but take longer to generate." placement="top" arrow>
|
||||
<TextField fullWidth type="number" label="Width" value={width} onChange={(e) => setWidth(parseInt(e.target.value || '0', 10))} sx={textInputSx} />
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item xs={6} md={2}>
|
||||
<Tooltip title="Image height in pixels. Recommended: 1024 for square images, 1080 for portrait covers. Aspect ratio affects composition and visual appeal." placement="top" arrow>
|
||||
<TextField fullWidth type="number" label="Height" value={height} onChange={(e) => setHeight(parseInt(e.target.value || '0', 10))} sx={textInputSx} />
|
||||
<Grid item xs={6} md={1.5}>
|
||||
<Tooltip
|
||||
title={`Image height in pixels. Max for ${model}: ${getMaxDimensionsForModel(model).maxHeight}px. Recommended: 1024 for square images, 1080 for portrait covers.`}
|
||||
placement="top"
|
||||
arrow
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
label="Height"
|
||||
value={height}
|
||||
onChange={(e) => {
|
||||
const newHeight = parseInt(e.target.value || '0', 10);
|
||||
const { maxHeight } = getMaxDimensionsForModel(model);
|
||||
setHeight(Math.min(newHeight, maxHeight));
|
||||
}}
|
||||
inputProps={{ min: 64, max: getMaxDimensionsForModel(model).maxHeight }}
|
||||
sx={textInputSx}
|
||||
error={height > getMaxDimensionsForModel(model).maxHeight}
|
||||
helperText={height > getMaxDimensionsForModel(model).maxHeight ? `Max: ${getMaxDimensionsForModel(model).maxHeight}px` : ''}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Cost Chip */}
|
||||
<Box sx={{ mt: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Chip
|
||||
label={`Estimated Cost: ${costInfo.cost}/image`}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
sx={{ fontSize: '12px', fontWeight: 500 }}
|
||||
/>
|
||||
<Typography variant="caption" sx={{ color: '#5f6368' }}>
|
||||
{costInfo.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Collapse>
|
||||
|
||||
{/* Model-Specific Guidance */}
|
||||
{(() => {
|
||||
const guidance = modelGuidance;
|
||||
if (guidance.tips.length === 0 && guidance.warnings.length === 0 && !guidance.recommendations) return null;
|
||||
|
||||
return (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
{guidance.warnings.length > 0 && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
icon={<InfoIcon />}
|
||||
sx={{
|
||||
mb: 1,
|
||||
backgroundColor: '#fff3cd',
|
||||
'& .MuiAlert-icon': { color: '#856404' },
|
||||
'& .MuiAlert-message': { color: '#856404' }
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
||||
Important Notes:
|
||||
</Typography>
|
||||
{guidance.warnings.map((warning, idx) => (
|
||||
<Typography key={idx} variant="body2" sx={{ fontSize: '13px', mb: 0.5 }}>
|
||||
• {warning}
|
||||
</Typography>
|
||||
))}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{guidance.tips.length > 0 && (
|
||||
<Alert
|
||||
severity="info"
|
||||
icon={<InfoIcon />}
|
||||
sx={{
|
||||
mb: guidance.recommendations ? 1 : 0,
|
||||
backgroundColor: '#e3f2fd',
|
||||
'& .MuiAlert-icon': { color: '#1976d2' },
|
||||
'& .MuiAlert-message': { color: '#1565c0' }
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
||||
💡 Best Practices for {model}:
|
||||
</Typography>
|
||||
{guidance.tips.map((tip, idx) => (
|
||||
<Typography key={idx} variant="body2" sx={{ fontSize: '13px', mb: 0.5 }}>
|
||||
• {tip}
|
||||
</Typography>
|
||||
))}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{guidance.recommendations && (
|
||||
<Alert
|
||||
severity="success"
|
||||
icon={<InfoIcon />}
|
||||
sx={{
|
||||
backgroundColor: '#d4edda',
|
||||
'& .MuiAlert-icon': { color: '#155724' },
|
||||
'& .MuiAlert-message': { color: '#155724' }
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
||||
✅ Recommendation:
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontSize: '13px' }}>
|
||||
{guidance.recommendations}
|
||||
</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Loading indicators */}
|
||||
{loadingSuggestions && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="caption" sx={{ mb: 0.5, display: 'block' }}>Loading suggestions...</Typography>
|
||||
<LinearProgress />
|
||||
<LinearProgress sx={{ height: 4, borderRadius: 2 }} />
|
||||
<Typography variant="caption" sx={{ mt: 0.5, display: 'block', color: '#5f6368' }}>
|
||||
Optimizing prompt...
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{isGenerating && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="caption" sx={{ mb: 0.5, display: 'block' }}>Generating image...</Typography>
|
||||
<LinearProgress />
|
||||
<LinearProgress sx={{ height: 4, borderRadius: 2 }} />
|
||||
<Typography variant="caption" sx={{ mt: 0.5, display: 'block', color: '#5f6368' }}>
|
||||
Generating image... This may take 10-30 seconds
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Prompt and Negative Prompt Side by Side - 80/20 split, stack on mobile */}
|
||||
<Box sx={{ mb: 2, display: { xs: 'block', md: 'flex' }, gap: 2 }}>
|
||||
<Tooltip title="Describe what you want in the image. Be specific: mention style (photorealistic, editorial, cinematic), subjects, composition, lighting, and mood. The AI uses this to generate your image. Tips: Include camera settings (e.g., '50mm lens, f/2.8'), lighting direction, and visual emphasis." placement="top" arrow>
|
||||
{/* Prompt Input with Optimize Button Inside */}
|
||||
<Box sx={{ mb: 2, position: 'relative' }}>
|
||||
<Tooltip
|
||||
title="Describe what you want in the image. Be specific: mention style (photorealistic, editorial, cinematic), subjects, composition, lighting, and mood. The AI uses this to generate your image."
|
||||
placement="top"
|
||||
arrow
|
||||
>
|
||||
<TextField
|
||||
sx={{ flex: { md: '0 0 80%' }, width: { xs: '100%' }, mb: { xs: 2, md: 0 } }}
|
||||
InputProps={{ sx: { color: '#202124' } }}
|
||||
InputLabelProps={{ sx: { color: '#5f6368' } }}
|
||||
FormHelperTextProps={{ sx: { color: '#5f6368' } }}
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={3}
|
||||
minRows={4}
|
||||
maxRows={8}
|
||||
label="Prompt"
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="Describe the image..."
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="Describe the image you want to generate. Be specific about style, composition, and mood..."
|
||||
sx={{
|
||||
...textInputSx,
|
||||
'& .MuiInputBase-root': {
|
||||
paddingRight: '140px', // Make room for button
|
||||
paddingBottom: '8px'
|
||||
}
|
||||
}}
|
||||
helperText="Tip: Include camera settings (e.g., '50mm lens, f/2.8'), lighting direction, and visual emphasis for better results."
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="List elements you want to avoid in the image (e.g., blurry, cartoon, watermark, low quality). This helps the AI exclude unwanted features. Common items: text artifacts, brand logos, distorted anatomy, oversaturation, noise." placement="top" arrow>
|
||||
{/* Optimize Prompt Button - Positioned inside textarea */}
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
bottom: '32px', // Position above helper text
|
||||
right: '14px',
|
||||
zIndex: 1
|
||||
}}>
|
||||
<Tooltip
|
||||
title="Get AI-generated prompt suggestions optimized for blog images. Focuses on data visualization, infographics, clean layouts with text overlay areas, and conceptual illustrations."
|
||||
placement="left"
|
||||
arrow
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={loadingSuggestions ? <CircularProgress size={14} /> : <AutoFixHighIcon />}
|
||||
onClick={suggestPrompt}
|
||||
disabled={!canOptimize}
|
||||
sx={{
|
||||
minWidth: 'auto',
|
||||
px: 1.5,
|
||||
py: 0.5,
|
||||
fontSize: '12px',
|
||||
textTransform: 'none',
|
||||
background: canOptimize
|
||||
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
||||
: '#f5f5f5',
|
||||
border: 'none',
|
||||
color: canOptimize ? '#ffffff' : '#9aa0a6',
|
||||
boxShadow: canOptimize
|
||||
? '0 2px 8px rgba(102, 126, 234, 0.3)'
|
||||
: 'none',
|
||||
'&:hover': {
|
||||
background: canOptimize
|
||||
? 'linear-gradient(135deg, #764ba2 0%, #667eea 100%)'
|
||||
: '#f5f5f5',
|
||||
boxShadow: canOptimize
|
||||
? '0 4px 12px rgba(102, 126, 234, 0.4)'
|
||||
: 'none',
|
||||
transform: canOptimize ? 'translateY(-1px)' : 'none'
|
||||
},
|
||||
'&:disabled': {
|
||||
background: '#f5f5f5',
|
||||
color: '#9aa0a6',
|
||||
border: 'none'
|
||||
},
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
{loadingSuggestions ? 'Optimizing...' : 'Optimize Prompt'}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Negative Prompt */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Tooltip
|
||||
title="List elements you want to avoid in the image (e.g., blurry, cartoon, watermark, low quality). This helps the AI exclude unwanted features."
|
||||
placement="top"
|
||||
arrow
|
||||
>
|
||||
<TextField
|
||||
sx={{ flex: { md: '0 0 20%' }, width: { xs: '100%' } }}
|
||||
InputProps={{ sx: { color: '#202124' } }}
|
||||
InputLabelProps={{ sx: { color: '#5f6368' } }}
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={3}
|
||||
minRows={2}
|
||||
maxRows={4}
|
||||
label="Negative Prompt (optional)"
|
||||
value={negative}
|
||||
onChange={(e) => setNegative(e.target.value)}
|
||||
onChange={(e) => setNegative(e.target.value)}
|
||||
placeholder="Elements to avoid: blurry, distorted, watermark, low quality..."
|
||||
sx={textInputSx}
|
||||
helperText="Common exclusions: text artifacts, brand logos, distorted anatomy, oversaturation, noise"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Tooltip title="Get AI-generated prompt suggestions tailored to your blog section. Uses your section title, subheadings, key points, keywords, and research data to create hyper-personalized prompts optimized for your chosen provider. Click to see multiple suggestions in tabs." placement="top" arrow>
|
||||
<span>
|
||||
<Button sx={{ mr: 1 }} variant="outlined" onClick={suggestPrompt} disabled={loadingSuggestions}>{loadingSuggestions ? 'Suggesting…' : 'Suggest prompt'}</Button>
|
||||
</span>
|
||||
{/* Generate Button */}
|
||||
<Box sx={{ mb: 2, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Tooltip
|
||||
title="Generate the image using your current prompt and settings. The process may take 10-30 seconds depending on provider and image size."
|
||||
placement="top"
|
||||
arrow
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!canGenerate}
|
||||
onClick={onGenerate}
|
||||
startIcon={isGenerating ? <CircularProgress size={18} color="inherit" /> : undefined}
|
||||
sx={{
|
||||
px: 3,
|
||||
py: 1.2,
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
textTransform: 'none',
|
||||
background: canGenerate
|
||||
? 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)'
|
||||
: 'linear-gradient(135deg, #e0e0e0 0%, #bdbdbd 100%)',
|
||||
border: 'none',
|
||||
color: canGenerate ? '#ffffff' : '#9e9e9e',
|
||||
boxShadow: canGenerate
|
||||
? '0 4px 15px rgba(102, 126, 234, 0.4)'
|
||||
: 'none',
|
||||
'&:hover': {
|
||||
background: canGenerate
|
||||
? 'linear-gradient(135deg, #764ba2 0%, #667eea 50%, #f093fb 100%)'
|
||||
: 'linear-gradient(135deg, #e0e0e0 0%, #bdbdbd 100%)',
|
||||
boxShadow: canGenerate
|
||||
? '0 6px 20px rgba(102, 126, 234, 0.5)'
|
||||
: 'none',
|
||||
transform: canGenerate ? 'translateY(-2px)' : 'none'
|
||||
},
|
||||
'&:disabled': {
|
||||
background: 'linear-gradient(135deg, #e0e0e0 0%, #bdbdbd 100%)',
|
||||
color: '#9e9e9e',
|
||||
boxShadow: 'none'
|
||||
},
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
{isGenerating ? 'Generating…' : 'Generate Image'}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
<Typography variant="body2">{error}</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Generated Image */}
|
||||
{result && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Card sx={{
|
||||
maxWidth: 512,
|
||||
mx: 'auto',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<CardMedia
|
||||
component="img"
|
||||
image={`data:image/png;base64,${result.image_base64}`}
|
||||
alt="Generated image"
|
||||
sx={{ width: '100%', height: 'auto' }}
|
||||
/>
|
||||
</Card>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Prompt Suggestions Tabs */}
|
||||
{suggestions.length > 0 && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600, color: '#202124' }}>
|
||||
Optimized Prompt Suggestions
|
||||
</Typography>
|
||||
<Tooltip
|
||||
title="Browse through AI-generated prompt suggestions. Each tab shows a different prompt optimized for your section and provider. Click a tab to preview and auto-fill the prompt fields."
|
||||
placement="top"
|
||||
arrow
|
||||
>
|
||||
<Tabs
|
||||
value={suggestionIndex}
|
||||
onChange={(e, v) => {
|
||||
setSuggestionIndex(v);
|
||||
const s = suggestions[v];
|
||||
if (s) {
|
||||
setPrompt(s.prompt || '');
|
||||
setNegative(s.negative_prompt || '');
|
||||
if (s.width) setWidth(s.width);
|
||||
if (s.height) setHeight(s.height);
|
||||
}
|
||||
}}
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
sx={{
|
||||
borderBottom: '1px solid #e8eaed',
|
||||
'& .MuiTab-root': {
|
||||
textTransform: 'none',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
minHeight: 40
|
||||
}
|
||||
}}
|
||||
>
|
||||
{suggestions.map((_, i) => (
|
||||
<Tab key={i} label={`Suggestion ${i + 1}`} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Tooltip>
|
||||
<Tooltip title="Generate the image using your current prompt and settings. The process may take 10-30 seconds depending on provider and image size. Once generated, the image will appear below and can be used for your blog section." placement="top" arrow>
|
||||
<span>
|
||||
<Button variant="contained" disabled={!canGenerate} onClick={onGenerate} startIcon={isGenerating ? <CircularProgress size={18} /> : undefined}>
|
||||
{isGenerating ? 'Generating…' : 'Generate Image'}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
{error && (
|
||||
<Grid item xs={12}>
|
||||
<Typography color="error" variant="body2">{error}</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
{result && (
|
||||
<Grid item xs={12}>
|
||||
<Card sx={{ maxWidth: 512 }}>
|
||||
<CardMedia component="img" image={`data:image/png;base64,${result.image_base64}`} alt="generated" />
|
||||
</Card>
|
||||
</Grid>
|
||||
)}
|
||||
{suggestions.length > 0 && (
|
||||
<Grid item xs={12}>
|
||||
<Tooltip title="Browse through AI-generated prompt suggestions. Each tab shows a different prompt optimized for your section and provider. Click a tab to preview and auto-fill the prompt fields. You can then modify or use it directly." placement="top" arrow>
|
||||
<div>
|
||||
<Tabs value={suggestionIndex} onChange={(e, v) => {
|
||||
setSuggestionIndex(v);
|
||||
const s = suggestions[v];
|
||||
if (s) {
|
||||
setPrompt(s.prompt || '');
|
||||
setNegative(s.negative_prompt || '');
|
||||
if (s.width) setWidth(s.width);
|
||||
if (s.height) setHeight(s.height);
|
||||
}
|
||||
}} variant="scrollable" scrollButtons allowScrollButtonsMobile>
|
||||
{suggestions.map((_, i) => (
|
||||
<Tab key={i} label={`Prompt ${i + 1}`} />
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip title="Preview of the currently selected prompt suggestion. Shows the main prompt and negative prompt (if any). This preview updates when you click different tabs above." placement="top" arrow>
|
||||
<Box sx={{ p: 2, border: '1px solid #e0e0e0', borderTop: 'none', borderRadius: '0 0 8px 8px', background: '#fff' }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>Preview</Typography>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', color: '#202124' }}>{suggestions[suggestionIndex]?.prompt}</Typography>
|
||||
{suggestions[suggestionIndex]?.negative_prompt && (
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary', display: 'block', mt: 1 }}>Negative: {suggestions[suggestionIndex]?.negative_prompt}</Typography>
|
||||
)}
|
||||
<Box sx={{
|
||||
p: 2,
|
||||
border: '1px solid #e8eaed',
|
||||
borderTop: 'none',
|
||||
borderRadius: '0 0 8px 8px',
|
||||
backgroundColor: '#f8f9fa'
|
||||
}}>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', color: '#202124', mb: 1 }}>
|
||||
{suggestions[suggestionIndex]?.prompt}
|
||||
</Typography>
|
||||
{suggestions[suggestionIndex]?.negative_prompt && (
|
||||
<Box sx={{ mt: 1.5, pt: 1.5, borderTop: '1px solid #e8eaed' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#5f6368', display: 'block', mb: 0.5 }}>
|
||||
Negative Prompt:
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#5f6368' }}>
|
||||
{suggestions[suggestionIndex]?.negative_prompt}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -65,34 +65,37 @@ const ImageGeneratorModal: React.FC<ImageGeneratorModalProps> = ({ isOpen, onClo
|
||||
<div style={modalStyle} onClick={(e) => e.stopPropagation()}>
|
||||
<div style={headerStyle}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<h3 style={{ margin: 0 }}>{sectionTitle}</h3>
|
||||
<span style={{ fontSize: 12, color: '#5f6368' }}>Generate Blog Section Image</span>
|
||||
<h3 style={{ margin: 0, fontSize: '18px', fontWeight: 600, color: '#202124' }}>{sectionTitle}</h3>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Tooltip title="Toggle advanced image generation settings. Opens provider selection (Hugging Face, Gemini, Stability AI), model specification, and image dimensions (width/height). Hover or click to show/hide these options." placement="bottom" arrow>
|
||||
<button
|
||||
onMouseEnter={() => imageRef.current?.openAdvanced()}
|
||||
onClick={() => {
|
||||
// toggle
|
||||
if (imageRef.current) {
|
||||
imageRef.current.openAdvanced();
|
||||
}
|
||||
}}
|
||||
style={{ border: '1px solid #cbd5e1', background: '#ffffff', color: '#334155', borderRadius: 20, padding: '6px 12px', cursor: 'pointer', boxShadow: '0 1px 2px rgba(0,0,0,0.04)' }}
|
||||
>
|
||||
Advanced Image Options
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Get AI-powered prompt suggestions tailored to your blog section. Uses section title, subheadings, key points, keywords, and research data to generate multiple hyper-personalized prompts. Suggestions appear as tabs below." placement="bottom" arrow>
|
||||
<button
|
||||
onClick={() => imageRef.current?.suggest()}
|
||||
style={{ border: '1px solid #1976d2', background: '#fff', color: '#1976d2', borderRadius: 20, padding: '6px 12px', cursor: 'pointer' }}
|
||||
>
|
||||
Suggest Prompt
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Close the image generator modal. Any generated images are saved and will appear in your blog section." placement="bottom" arrow>
|
||||
<button onClick={onClose} style={{ border: '1px solid #ddd', background: '#f5f5f5', borderRadius: 6, padding: '6px 10px', cursor: 'pointer' }}>Close</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%)',
|
||||
color: '#5f6368',
|
||||
borderRadius: 8,
|
||||
padding: '8px 20px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, #e8eaed 0%, #dadce0 100%)';
|
||||
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 8px rgba(0,0,0,0.15)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%)';
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { apiClient } from '../../api/client';
|
||||
export interface ImageGenerationRequest {
|
||||
prompt: string;
|
||||
negative_prompt?: string;
|
||||
provider?: 'gemini' | 'huggingface' | 'stability';
|
||||
provider?: 'gemini' | 'huggingface' | 'stability' | 'wavespeed';
|
||||
model?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
@@ -31,12 +31,34 @@ export function useImageGeneration() {
|
||||
const generate = useCallback(async (req: ImageGenerationRequest) => {
|
||||
setIsGenerating(true);
|
||||
setError(null);
|
||||
setResult(null);
|
||||
try {
|
||||
const { data } = await apiClient.post<ImageGenerationResponse>('/api/images/generate', req);
|
||||
setResult(data);
|
||||
return data;
|
||||
const response = await apiClient.post<ImageGenerationResponse>('/api/images/generate', req);
|
||||
const data = response.data;
|
||||
|
||||
// Check if response has success field and image data
|
||||
if (data && (data.success !== false) && data.image_base64) {
|
||||
setResult(data);
|
||||
setError(null);
|
||||
return data;
|
||||
} else {
|
||||
// Response received but missing required data
|
||||
const message = 'Image generation completed but response is incomplete';
|
||||
setError(message);
|
||||
throw new Error(message);
|
||||
}
|
||||
} catch (e: any) {
|
||||
const message = e?.response?.data?.detail || e?.message || 'Image generation failed';
|
||||
// Check if error response contains image data (partial success)
|
||||
if (e?.response?.data?.image_base64) {
|
||||
// Image was generated but there was an error in post-processing
|
||||
const data = e.response.data;
|
||||
console.warn('Image generation succeeded but post-processing had issues', data);
|
||||
setResult(data);
|
||||
setError(null);
|
||||
return data;
|
||||
}
|
||||
|
||||
const message = e?.response?.data?.detail || e?.response?.data?.message || e?.message || 'Image generation failed';
|
||||
setError(message);
|
||||
throw new Error(message);
|
||||
} finally {
|
||||
@@ -55,7 +77,15 @@ export interface PromptSuggestion {
|
||||
overlay_text?: string;
|
||||
}
|
||||
|
||||
export async function fetchPromptSuggestions(payload: any): Promise<PromptSuggestion[]> {
|
||||
export async function fetchPromptSuggestions(payload: {
|
||||
provider?: string;
|
||||
model?: string;
|
||||
image_type?: string;
|
||||
title?: string;
|
||||
section?: any;
|
||||
research?: any;
|
||||
persona?: any;
|
||||
}): Promise<PromptSuggestion[]> {
|
||||
// Use apiClient directly (same pattern as SEO analysis in SEOAnalysisModal.tsx)
|
||||
// The apiClient interceptor will handle auth token injection automatically
|
||||
const response = await apiClient.post('/api/images/suggest-prompts', payload);
|
||||
|
||||
Reference in New Issue
Block a user