Files
igny8/frontend/src/pages/account/ContentSettingsPage.tsx
IGNY8 VPS (Salman) add04e2ad5 Section 2 COmpleted
2025-12-27 02:20:55 +00:00

667 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Content Settings Page - 3 Tabs
* Tabs: Content Generation, Publishing, Image Settings
* Consolidated settings for content creation workflow
*/
import { useState, useEffect, useCallback } from 'react';
import {
Save, Loader2, Image as ImageIcon, FileText, Send, Settings
} from 'lucide-react';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import { fetchAPI } from '../../services/api';
import { useToast } from '../../components/ui/toast/ToastContainer';
import SelectDropdown from '../../components/form/SelectDropdown';
import Label from '../../components/form/Label';
import Checkbox from '../../components/form/input/Checkbox';
import PageMeta from '../../components/common/PageMeta';
type TabType = 'content' | 'publishing' | 'images';
interface ImageGenerationSettings {
enabled: boolean;
service: 'openai' | 'runware';
provider: string;
model: string;
runwareModel?: string;
image_type: 'realistic' | 'artistic' | 'cartoon';
max_in_article_images: number;
image_format: 'webp' | 'jpg' | 'png';
desktop_enabled: boolean;
mobile_enabled: boolean;
featured_image_size: string;
desktop_image_size: string;
}
interface PublishingSettings {
autoPublishEnabled: boolean;
autoSyncEnabled: boolean;
}
interface ContentGenerationSettings {
appendToPrompt: string;
defaultTone: string;
defaultLength: string;
}
// Map user-friendly quality to internal service/model configuration
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' },
};
// Map internal config back to user-friendly quality
const getQualityFromConfig = (service?: string, model?: string): 'standard' | 'premium' | 'best' => {
if (service === 'runware') return 'best';
if (model === 'dall-e-3') return 'premium';
return 'standard';
};
// Get available image sizes based on provider and model
const getImageSizes = (provider: string, model: string) => {
if (provider === 'runware') {
return [
{ value: '1280x832', label: '1280×832 pixels' },
{ value: '1024x1024', label: '1024×1024 pixels' },
{ value: '512x512', label: '512×512 pixels' },
];
} 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' }];
};
export default function ContentSettingsPage() {
const toast = useToast();
const [activeTab, setActiveTab] = useState<TabType>('content');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
// Content Generation Settings
const [contentSettings, setContentSettings] = useState<ContentGenerationSettings>({
appendToPrompt: '',
defaultTone: 'professional',
defaultLength: 'medium',
});
// Publishing Settings
const [publishingSettings, setPublishingSettings] = useState<PublishingSettings>({
autoPublishEnabled: false,
autoSyncEnabled: false,
});
// Image Quality
const [imageQuality, setImageQuality] = useState<'standard' | 'premium' | 'best'>('premium');
// Image Generation Settings
const [imageSettings, setImageSettings] = useState<ImageGenerationSettings>({
enabled: true,
service: 'openai',
provider: 'openai',
model: 'dall-e-3',
image_type: 'realistic',
max_in_article_images: 2,
image_format: 'webp',
desktop_enabled: true,
mobile_enabled: true,
featured_image_size: '1024x1024',
desktop_image_size: '1024x1024',
});
// Get current provider/model from quality setting
const getCurrentConfig = useCallback(() => {
const config = QUALITY_TO_CONFIG[imageQuality];
return {
service: config.service,
model: config.model,
};
}, [imageQuality]);
// Get available sizes for current quality
const availableSizes = getImageSizes(
getCurrentConfig().service,
getCurrentConfig().model
);
useEffect(() => {
loadSettings();
}, []);
// Update image sizes when quality changes
useEffect(() => {
const config = getCurrentConfig();
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);
const needsDesktopUpdate = !validSizes.includes(imageSettings.desktop_image_size);
if (needsFeaturedUpdate || needsDesktopUpdate) {
setImageSettings(prev => ({
...prev,
service: config.service,
provider: config.service,
model: config.model,
featured_image_size: needsFeaturedUpdate ? defaultSize : prev.featured_image_size,
desktop_image_size: needsDesktopUpdate ? defaultSize : prev.desktop_image_size,
}));
} else {
setImageSettings(prev => ({
...prev,
service: config.service,
provider: config.service,
model: config.model,
}));
}
}, [imageQuality, getCurrentConfig]);
const loadSettings = async () => {
try {
setLoading(true);
// Load image generation settings
const imageData = await fetchAPI('/v1/system/settings/integrations/image_generation/');
if (imageData) {
const quality = getQualityFromConfig(imageData.service || imageData.provider, imageData.model);
setImageQuality(quality);
setImageSettings({
enabled: imageData.enabled !== false,
service: imageData.service || imageData.provider || 'openai',
provider: imageData.provider || imageData.service || 'openai',
model: imageData.model || 'dall-e-3',
runwareModel: imageData.runwareModel,
image_type: imageData.image_type || 'realistic',
max_in_article_images: imageData.max_in_article_images || 2,
image_format: imageData.image_format || 'webp',
desktop_enabled: imageData.desktop_enabled !== false,
mobile_enabled: imageData.mobile_enabled !== false,
featured_image_size: imageData.featured_image_size || '1024x1024',
desktop_image_size: imageData.desktop_image_size || '1024x1024',
});
}
// Load content generation settings
try {
const contentData = await fetchAPI('/v1/system/settings/content/content_generation/');
if (contentData?.config) {
setContentSettings({
appendToPrompt: contentData.config.append_to_prompt || '',
defaultTone: contentData.config.default_tone || 'professional',
defaultLength: contentData.config.default_length || 'medium',
});
}
} catch (err) {
// Settings may not exist yet, use defaults
console.log('Content generation settings not found, using defaults');
}
// Load publishing settings
try {
const publishData = await fetchAPI('/v1/system/settings/content/publishing/');
if (publishData?.config) {
setPublishingSettings({
autoPublishEnabled: publishData.config.auto_publish_enabled || false,
autoSyncEnabled: publishData.config.auto_sync_enabled || false,
});
}
} catch (err) {
// Settings may not exist yet, use defaults
console.log('Publishing settings not found, using defaults');
}
} catch (error: any) {
console.error('Error loading content settings:', error);
} finally {
setLoading(false);
}
};
const handleSave = async () => {
try {
setSaving(true);
if (activeTab === 'images') {
const config = getCurrentConfig();
const configToSave = {
enabled: imageSettings.enabled,
service: config.service,
provider: config.service,
model: config.model,
runwareModel: config.service === 'runware' ? config.model : undefined,
image_type: imageSettings.image_type,
max_in_article_images: imageSettings.max_in_article_images,
image_format: imageSettings.image_format,
desktop_enabled: imageSettings.desktop_enabled,
mobile_enabled: imageSettings.mobile_enabled,
featured_image_size: imageSettings.featured_image_size,
desktop_image_size: imageSettings.desktop_image_size,
};
await fetchAPI('/v1/system/settings/integrations/image_generation/save/', {
method: 'POST',
body: JSON.stringify(configToSave),
});
}
// Save content generation settings
if (activeTab === 'content') {
await fetchAPI('/v1/system/settings/content/content_generation/save/', {
method: 'POST',
body: JSON.stringify({
config: {
append_to_prompt: contentSettings.appendToPrompt,
default_tone: contentSettings.defaultTone,
default_length: contentSettings.defaultLength,
}
}),
});
}
// Save publishing settings
if (activeTab === 'publishing') {
await fetchAPI('/v1/system/settings/content/publishing/save/', {
method: 'POST',
body: JSON.stringify({
config: {
auto_publish_enabled: publishingSettings.autoPublishEnabled,
auto_sync_enabled: publishingSettings.autoSyncEnabled,
}
}),
});
}
toast.success('Settings saved successfully');
} catch (error: any) {
console.error('Error saving settings:', error);
toast.error(`Failed to save settings: ${error.message}`);
} finally {
setSaving(false);
}
};
const tabs = [
{ id: 'content' as TabType, label: 'Content Generation', icon: <FileText className="w-4 h-4" /> },
{ id: 'publishing' as TabType, label: 'Publishing', icon: <Send className="w-4 h-4" /> },
{ id: 'images' as TabType, label: 'Image Settings', icon: <ImageIcon className="w-4 h-4" /> },
];
if (loading) {
return (
<div className="p-6">
<PageMeta title="Content Settings" description="Configure your content generation settings" />
<div className="flex items-center justify-center h-64">
<div className="flex flex-col items-center gap-3">
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-brand-500)]" />
<div className="text-gray-500 dark:text-gray-400">Loading settings...</div>
</div>
</div>
</div>
);
}
return (
<div className="p-6">
<PageMeta title="Content Settings" description="Configure your content generation settings" />
{/* Page Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Content Settings</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Configure how your content and images are generated
</p>
</div>
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8">
{tabs.map((tab) => (
<button
type="button"
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`
flex items-center gap-2 py-4 px-1 border-b-2 font-medium text-sm
${activeTab === tab.id
? 'border-[var(--color-brand-500)] text-[var(--color-brand-600)] dark:text-[var(--color-brand-400)]'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}
`}
>
{tab.icon}
{tab.label}
</button>
))}
</nav>
</div>
{/* Tab Content */}
<div className="mt-6">
{/* Content Generation Tab */}
{activeTab === 'content' && (
<div className="space-y-6 max-w-4xl">
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
<FileText className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Content Generation</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">Customize how your articles are written</p>
</div>
</div>
<div className="space-y-6">
<div>
<Label className="mb-2">Append to Every Prompt</Label>
<textarea
value={contentSettings.appendToPrompt}
onChange={(e) => setContentSettings({ ...contentSettings, appendToPrompt: e.target.value })}
placeholder="Add custom instructions that will be included with every content generation request..."
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800 min-h-[120px] resize-y"
/>
<p className="text-xs text-gray-500 mt-1">
This text will be appended to every AI prompt. Use it to enforce brand guidelines, tone, or specific requirements.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Label className="mb-2">Default Writing Tone</Label>
<SelectDropdown
options={[
{ value: 'professional', label: 'Professional' },
{ value: 'conversational', label: 'Conversational' },
{ value: 'formal', label: 'Formal' },
{ value: 'casual', label: 'Casual' },
{ value: 'friendly', label: 'Friendly' },
]}
value={contentSettings.defaultTone}
onChange={(value) => setContentSettings({ ...contentSettings, defaultTone: value })}
className="w-full"
/>
</div>
<div>
<Label className="mb-2">Default Article Length</Label>
<SelectDropdown
options={[
{ value: 'short', label: 'Short (500-800 words)' },
{ value: 'medium', label: 'Medium (1000-1500 words)' },
{ value: 'long', label: 'Long (2000-3000 words)' },
{ value: 'comprehensive', label: 'Comprehensive (3000+ words)' },
]}
value={contentSettings.defaultLength}
onChange={(value) => setContentSettings({ ...contentSettings, defaultLength: value })}
className="w-full"
/>
</div>
</div>
</div>
</Card>
{/* Save Button */}
<div className="flex justify-end">
<Button
variant="primary"
tone="brand"
onClick={handleSave}
disabled={saving}
startIcon={saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
>
{saving ? 'Saving...' : 'Save Settings'}
</Button>
</div>
</div>
)}
{/* Publishing Tab */}
{activeTab === 'publishing' && (
<div className="space-y-6 max-w-4xl">
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg">
<Send className="w-5 h-5 text-green-600 dark:text-green-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">WordPress Publishing</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">Configure automatic publishing to your WordPress sites</p>
</div>
</div>
<div className="space-y-6">
{/* Auto-Publish Setting */}
<div className="p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-start gap-3">
<Checkbox
checked={publishingSettings.autoPublishEnabled}
onChange={(checked) => setPublishingSettings({ ...publishingSettings, autoPublishEnabled: checked })}
/>
<div className="flex-1">
<Label className="font-medium text-gray-900 dark:text-white">
Automatic Publishing
</Label>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Automatically publish articles to WordPress when they're finished and reviewed
</p>
</div>
</div>
{publishingSettings.autoPublishEnabled && (
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<p className="text-sm text-blue-800 dark:text-blue-200">
Articles will be published automatically once they pass review. You can still manually review them first if needed.
</p>
</div>
)}
</div>
{/* Auto-Sync Setting */}
<div className="p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-start gap-3">
<Checkbox
checked={publishingSettings.autoSyncEnabled}
onChange={(checked) => setPublishingSettings({ ...publishingSettings, autoSyncEnabled: checked })}
/>
<div className="flex-1">
<Label className="font-medium text-gray-900 dark:text-white">
Keep Content Updated
</Label>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Automatically update articles on WordPress if you make changes here
</p>
</div>
</div>
</div>
</div>
</Card>
{/* Save Button */}
<div className="flex justify-end">
<Button
variant="primary"
tone="brand"
onClick={handleSave}
disabled={saving}
startIcon={saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
>
{saving ? 'Saving...' : 'Save Settings'}
</Button>
</div>
</div>
)}
{/* Image Settings Tab */}
{activeTab === 'images' && (
<div className="space-y-6 max-w-4xl">
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
<ImageIcon className="w-5 h-5 text-purple-600 dark:text-purple-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Image Generation</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">Configure how images are created for your articles</p>
</div>
</div>
<div className="space-y-6">
{/* Row 1: Image 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>
</div>
<div>
<Label className="mb-2">Image Style</Label>
<SelectDropdown
options={[
{ value: 'realistic', label: 'Realistic' },
{ value: 'artistic', label: 'Artistic' },
{ value: 'cartoon', label: 'Cartoon' },
]}
value={imageSettings.image_type}
onChange={(value) => setImageSettings({ ...imageSettings, image_type: value as any })}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">
Choose the visual style that matches your brand
</p>
</div>
</div>
{/* Row 2: Featured Image Size */}
<div>
<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-blue-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">Always Enabled</div>
</div>
<SelectDropdown
options={availableSizes}
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"
/>
</div>
</div>
{/* Row 3: Desktop & Mobile Images */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 rounded-lg border border-gray-200 dark:border-gray-700 space-y-3">
<div className="flex items-center gap-3">
<Checkbox
checked={imageSettings.desktop_enabled}
onChange={(checked) => setImageSettings({ ...imageSettings, desktop_enabled: checked })}
/>
<Label className="font-medium text-gray-700 dark:text-gray-300">
Desktop Images
</Label>
</div>
{imageSettings.desktop_enabled && (
<SelectDropdown
options={availableSizes}
value={imageSettings.desktop_image_size}
onChange={(value) => setImageSettings({ ...imageSettings, desktop_image_size: value })}
className="w-full"
/>
)}
</div>
<div className="flex items-center gap-3 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<Checkbox
checked={imageSettings.mobile_enabled}
onChange={(checked) => setImageSettings({ ...imageSettings, mobile_enabled: checked })}
/>
<div>
<Label className="font-medium text-gray-700 dark:text-gray-300">
Mobile Images
</Label>
<div className="text-xs text-gray-500 dark:text-gray-400">
512×512 pixels
</div>
</div>
</div>
</div>
{/* Row 4: Max Images & Format */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Label className="mb-2">Max In-Article Images</Label>
<SelectDropdown
options={[
{ value: '1', label: '1 Image' },
{ value: '2', label: '2 Images' },
{ value: '3', label: '3 Images' },
{ value: '4', label: '4 Images' },
{ value: '5', label: '5 Images' },
]}
value={String(imageSettings.max_in_article_images)}
onChange={(value) => setImageSettings({ ...imageSettings, max_in_article_images: parseInt(value) })}
className="w-full"
/>
</div>
<div>
<Label className="mb-2">Image Format</Label>
<SelectDropdown
options={[
{ value: 'webp', label: 'WEBP (recommended)' },
{ value: 'jpg', label: 'JPG' },
{ value: 'png', label: 'PNG' },
]}
value={imageSettings.image_format}
onChange={(value) => setImageSettings({ ...imageSettings, image_format: value as any })}
className="w-full"
/>
</div>
</div>
</div>
</Card>
{/* Save Button */}
<div className="flex justify-end">
<Button
variant="primary"
tone="brand"
onClick={handleSave}
disabled={saving}
startIcon={saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
>
{saving ? 'Saving...' : 'Save Settings'}
</Button>
</div>
</div>
)}
</div>
</div>
);
}