SEction 2 part 2

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-03 04:39:06 +00:00
parent 94d37a0d84
commit 935c7234b1
11 changed files with 1424 additions and 44 deletions

View File

@@ -3,7 +3,7 @@
* Phase 7: Advanced Site Management
* Features: SEO (meta tags, Open Graph, schema.org), Industry & Sectors Configuration
*/
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
@@ -27,7 +27,7 @@ import {
} from '../../services/api';
import WordPressIntegrationForm from '../../components/sites/WordPressIntegrationForm';
import { integrationApi, SiteIntegration } from '../../services/integration.api';
import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon, CloseIcon, PlusIcon, RefreshCwIcon } from '../../icons';
import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon, CloseIcon, PlusIcon, RefreshCwIcon, FileTextIcon, ImageIcon, SaveIcon, Loader2Icon } from '../../icons';
import Badge from '../../components/ui/badge/Badge';
import { Dropdown } from '../../components/ui/dropdown/Dropdown';
import { DropdownItem } from '../../components/ui/dropdown/DropdownItem';
@@ -49,9 +49,9 @@ export default function SiteSettings() {
const [isSiteSelectorOpen, setIsSiteSelectorOpen] = useState(false);
const siteSelectorRef = useRef<HTMLButtonElement>(null);
// Check for tab parameter in URL
const initialTab = (searchParams.get('tab') as 'general' | 'integrations' | 'publishing' | 'content-types') || 'general';
const [activeTab, setActiveTab] = useState<'general' | 'integrations' | 'publishing' | 'content-types'>(initialTab);
// Check for tab parameter in URL - now includes content-generation and image-settings tabs
const initialTab = (searchParams.get('tab') as 'general' | 'content-generation' | 'image-settings' | 'integrations' | 'publishing' | 'content-types') || 'general';
const [activeTab, setActiveTab] = useState<'general' | 'content-generation' | 'image-settings' | 'integrations' | 'publishing' | 'content-types'>(initialTab);
const [contentTypes, setContentTypes] = useState<any>(null);
const [contentTypesLoading, setContentTypesLoading] = useState(false);
@@ -60,6 +60,79 @@ export default function SiteSettings() {
const [publishingSettingsLoading, setPublishingSettingsLoading] = useState(false);
const [publishingSettingsSaving, setPublishingSettingsSaving] = useState(false);
// Content Generation Settings state
const [contentGenerationSettings, setContentGenerationSettings] = useState({
appendToPrompt: '',
defaultTone: 'professional',
defaultLength: 'medium',
});
const [contentGenerationLoading, setContentGenerationLoading] = useState(false);
const [contentGenerationSaving, setContentGenerationSaving] = useState(false);
// Image Settings state
const [imageQuality, setImageQuality] = useState<'standard' | 'premium' | 'best'>('premium');
const [imageSettings, setImageSettings] = useState({
enabled: true,
service: 'openai' as 'openai' | 'runware',
provider: 'openai',
model: 'dall-e-3',
image_type: 'realistic' as 'realistic' | 'artistic' | 'cartoon',
max_in_article_images: 2,
image_format: 'webp' as 'webp' | 'jpg' | 'png',
desktop_enabled: true,
mobile_enabled: true,
featured_image_size: '1024x1024',
desktop_image_size: '1024x1024',
});
const [imageSettingsLoading, setImageSettingsLoading] = useState(false);
const [imageSettingsSaving, setImageSettingsSaving] = useState(false);
// Image quality to config mapping
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' },
};
const getQualityFromConfig = (service?: string, model?: string): 'standard' | 'premium' | 'best' => {
if (service === 'runware') return 'best';
if (model === 'dall-e-3') return 'premium';
return 'standard';
};
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' }];
};
const getCurrentImageConfig = useCallback(() => {
const config = QUALITY_TO_CONFIG[imageQuality];
return { service: config.service, model: config.model };
}, [imageQuality]);
const availableImageSizes = getImageSizes(
getCurrentImageConfig().service,
getCurrentImageConfig().model
);
// Sectors selection state
const [industries, setIndustries] = useState<Industry[]>([]);
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
@@ -111,7 +184,7 @@ export default function SiteSettings() {
useEffect(() => {
// Update tab if URL parameter changes
const tab = searchParams.get('tab');
if (tab && ['general', 'integrations', 'publishing', 'content-types'].includes(tab)) {
if (tab && ['general', 'content-generation', 'image-settings', 'integrations', 'publishing', 'content-types'].includes(tab)) {
setActiveTab(tab as typeof activeTab);
}
}, [searchParams]);
@@ -128,6 +201,49 @@ export default function SiteSettings() {
}
}, [activeTab, siteId]);
// Load content generation settings when tab is active
useEffect(() => {
if (activeTab === 'content-generation' && siteId) {
loadContentGenerationSettings();
}
}, [activeTab, siteId]);
// Load image settings when tab is active
useEffect(() => {
if (activeTab === 'image-settings' && siteId) {
loadImageSettings();
}
}, [activeTab, siteId]);
// Update image sizes when quality changes
useEffect(() => {
const config = getCurrentImageConfig();
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, getCurrentImageConfig]);
// Load sites for selector
useEffect(() => {
loadSites();
@@ -253,6 +369,109 @@ export default function SiteSettings() {
}
};
// Content Generation Settings
const loadContentGenerationSettings = async () => {
try {
setContentGenerationLoading(true);
const contentData = await fetchAPI('/v1/system/settings/content/content_generation/');
if (contentData?.config) {
setContentGenerationSettings({
appendToPrompt: contentData.config.append_to_prompt || '',
defaultTone: contentData.config.default_tone || 'professional',
defaultLength: contentData.config.default_length || 'medium',
});
}
} catch (err) {
console.log('Content generation settings not found, using defaults');
} finally {
setContentGenerationLoading(false);
}
};
const saveContentGenerationSettings = async () => {
try {
setContentGenerationSaving(true);
await fetchAPI('/v1/system/settings/content/content_generation/save/', {
method: 'POST',
body: JSON.stringify({
config: {
append_to_prompt: contentGenerationSettings.appendToPrompt,
default_tone: contentGenerationSettings.defaultTone,
default_length: contentGenerationSettings.defaultLength,
}
}),
});
toast.success('Content generation settings saved successfully');
} catch (error: any) {
console.error('Error saving content generation settings:', error);
toast.error(`Failed to save settings: ${error.message}`);
} finally {
setContentGenerationSaving(false);
}
};
// Image Settings
const loadImageSettings = async () => {
try {
setImageSettingsLoading(true);
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',
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',
});
}
} catch (error: any) {
console.error('Error loading image settings:', error);
} finally {
setImageSettingsLoading(false);
}
};
const saveImageSettings = async () => {
try {
setImageSettingsSaving(true);
const config = getCurrentImageConfig();
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),
});
toast.success('Image settings saved successfully');
} catch (error: any) {
console.error('Error saving image settings:', error);
toast.error(`Failed to save settings: ${error.message}`);
} finally {
setImageSettingsSaving(false);
}
};
const loadIndustries = async () => {
try {
const response = await fetchIndustries();
@@ -609,14 +828,14 @@ export default function SiteSettings() {
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex gap-4">
<div className="flex gap-4 overflow-x-auto">
<Button
variant="ghost"
onClick={() => {
setActiveTab('general');
navigate(`/sites/${siteId}/settings`, { replace: true });
}}
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
activeTab === 'general'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
@@ -625,13 +844,43 @@ export default function SiteSettings() {
>
General
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab('content-generation');
navigate(`/sites/${siteId}/settings?tab=content-generation`, { replace: true });
}}
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
activeTab === 'content-generation'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
startIcon={<FileTextIcon className="w-4 h-4" />}
>
Content
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab('image-settings');
navigate(`/sites/${siteId}/settings?tab=image-settings`, { replace: true });
}}
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
activeTab === 'image-settings'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
startIcon={<ImageIcon className="w-4 h-4" />}
>
Images
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab('integrations');
navigate(`/sites/${siteId}/settings?tab=integrations`, { replace: true });
}}
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
activeTab === 'integrations'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
@@ -646,7 +895,7 @@ export default function SiteSettings() {
setActiveTab('publishing');
navigate(`/sites/${siteId}/settings?tab=publishing`, { replace: true });
}}
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
activeTab === 'publishing'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
@@ -662,7 +911,7 @@ export default function SiteSettings() {
setActiveTab('content-types');
navigate(`/sites/${siteId}/settings?tab=content-types`, { replace: true });
}}
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
activeTab === 'content-types'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
@@ -675,6 +924,241 @@ export default function SiteSettings() {
</div>
</div>
{/* Content Generation Tab */}
{activeTab === 'content-generation' && (
<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-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>
{contentGenerationLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2Icon className="w-8 h-8 animate-spin text-brand-500" />
</div>
) : (
<div className="space-y-6">
<div>
<Label className="mb-2">Append to Every Prompt</Label>
<TextArea
value={contentGenerationSettings.appendToPrompt}
onChange={(value) => setContentGenerationSettings({ ...contentGenerationSettings, 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={contentGenerationSettings.defaultTone}
onChange={(value) => setContentGenerationSettings({ ...contentGenerationSettings, 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={contentGenerationSettings.defaultLength}
onChange={(value) => setContentGenerationSettings({ ...contentGenerationSettings, defaultLength: value })}
className="w-full"
/>
</div>
</div>
</div>
)}
</Card>
<div className="flex justify-end">
<Button
variant="primary"
tone="brand"
onClick={saveContentGenerationSettings}
disabled={contentGenerationSaving}
startIcon={contentGenerationSaving ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon className="w-4 h-4" />}
>
{contentGenerationSaving ? 'Saving...' : 'Save Settings'}
</Button>
</div>
</div>
)}
{/* Image Settings Tab */}
{activeTab === 'image-settings' && (
<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>
{imageSettingsLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2Icon className="w-8 h-8 animate-spin text-purple-500" />
</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-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={availableImageSizes}
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={availableImageSizes}
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>
<div className="flex justify-end">
<Button
variant="primary"
tone="brand"
onClick={saveImageSettings}
disabled={imageSettingsSaving}
startIcon={imageSettingsSaving ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon className="w-4 h-4" />}
>
{imageSettingsSaving ? 'Saving...' : 'Save Settings'}
</Button>
</div>
</div>
)}
{/* Publishing Tab */}
{activeTab === 'publishing' && (
<Card>