Files
igny8/frontend/src/pages/account/ContentSettingsPage.tsx
IGNY8 VPS (Salman) 75deda304e reanme purple to info
2026-01-24 15:27:51 +00:00

687 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 { useLocation } from 'react-router-dom';
import {
SaveIcon, Loader2Icon, ImageIcon, FileTextIcon, PaperPlaneIcon, SettingsIcon
} from '../../icons';
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 TextArea from '../../components/form/input/TextArea';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import { BoxCubeIcon } from '../../icons';
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;
}
// AI Model Config from API
interface AIModelConfig {
model_name: string;
display_name: string;
model_type: string;
provider: string;
valid_sizes?: string[];
quality_tier?: string;
credits_per_image?: number;
}
// 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 (from API or fallback)
const getImageSizes = (provider: string, model: string, imageModels: AIModelConfig[]) => {
// First, try to find the model in the fetched models
const modelConfig = imageModels.find(m =>
m.model_name === model ||
(m.provider === provider && m.model_name.includes(model))
);
// If found and has valid_sizes, use them
if (modelConfig?.valid_sizes && modelConfig.valid_sizes.length > 0) {
return modelConfig.valid_sizes.map(size => ({
value: size,
label: `${size.replace('x', '×')} pixels`
}));
}
// Fallback to hardcoded sizes for backward compatibility
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' }];
};
// Get tab from URL path
function getTabFromPath(pathname: string): TabType {
if (pathname.includes('/publishing')) return 'publishing';
if (pathname.includes('/images')) return 'images';
return 'content';
}
export default function ContentSettingsPage() {
const toast = useToast();
const location = useLocation();
const activeTab = getTabFromPath(location.pathname);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
// Image Models from API - for dynamic size options
const [imageModels, setImageModels] = useState<AIModelConfig[]>([]);
// 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 (uses imageModels from API)
const availableSizes = getImageSizes(
getCurrentConfig().service,
getCurrentConfig().model,
imageModels
);
useEffect(() => {
loadSettings();
}, []);
// Update image sizes when quality changes or imageModels are loaded
useEffect(() => {
const config = getCurrentConfig();
const sizes = getImageSizes(config.service, config.model, imageModels);
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, imageModels]);
const loadSettings = async () => {
try {
setLoading(true);
// Load available image models from API (for dynamic sizes)
try {
const modelsResponse = await fetchAPI('/v1/billing/models/?type=image');
if (modelsResponse?.data) {
setImageModels(modelsResponse.data);
} else if (Array.isArray(modelsResponse)) {
setImageModels(modelsResponse);
}
} catch (err) {
console.log('Image models not available, using hardcoded sizes');
}
// 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 tabTitles: Record<TabType, string> = {
content: 'Content Generation',
publishing: 'Publishing',
images: 'Image Settings',
};
return (
<div className="p-6">
<PageMeta title="Content Settings" description="Configure your content generation settings" />
<PageHeader
title={tabTitles[activeTab]}
description={
activeTab === 'content' ? 'Customize how your articles are written' :
activeTab === 'publishing' ? 'Configure automatic publishing settings' :
'Set up AI image generation preferences'
}
badge={{ icon: <BoxCubeIcon />, color: 'blue' }}
parent="Content Settings"
/>
{/* Tab Content */}
<div className="mt-6">
{/* Content Generation Tab */}
{activeTab === 'content' && (
<div className="space-y-6 max-w-4xl">
<Card className="p-6 border-l-4 border-l-brand-500">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-brand-100 dark:bg-brand-900/30 rounded-lg">
<FileTextIcon className="w-5 h-5 text-brand-600 dark:text-brand-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={(value) => setContentSettings({ ...contentSettings, appendToPrompt: value })}
placeholder="Add custom instructions that will be included with every content generation request..."
rows={5}
/>
<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 ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon 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 border-l-4 border-l-success-500">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-success-100 dark:bg-success-900/30 rounded-lg">
<PaperPlaneIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Publishing to your sites</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">Configure automatic publishing to your 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 your site when they're finished and reviewed
</p>
</div>
</div>
{publishingSettings.autoPublishEnabled && (
<div className="mt-4 p-3 bg-brand-50 dark:bg-brand-900/20 border border-brand-200 dark:border-brand-800 rounded-lg">
<p className="text-sm text-brand-800 dark:text-brand-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 your site 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 ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon 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 border-l-4 border-l-info-500">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-info-100 dark:bg-info-900/30 rounded-lg">
<ImageIcon className="w-5 h-5 text-info-600 dark:text-info-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-info-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">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 ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon className="w-4 h-4" />}
>
{saving ? 'Saving...' : 'Save Settings'}
</Button>
</div>
</div>
)}
</div>
</div>
);
}