+
{section.label && (
{
+ return fetchAPI(`/v1/writer/content/${contentId}/schedule/`, {
+ method: 'POST',
+ body: JSON.stringify({ scheduled_publish_at: scheduledDate }),
+ });
+}
+
+// API function to unschedule content
+async function unscheduleContent(contentId: number): Promise {
+ return fetchAPI(`/v1/writer/content/${contentId}/unschedule/`, {
+ method: 'POST',
+ });
+}
+
+export default function ContentCalendar() {
+ const navigate = useNavigate();
+ const toast = useToast();
+ const { activeSite } = useSiteStore();
+
+ const [loading, setLoading] = useState(true);
+ const [allContent, setAllContent] = useState([]);
+ const [viewMode, setViewMode] = useState('calendar'); // Default to calendar view
+ const [draggedItem, setDraggedItem] = useState(null);
+
+ // Derived state: Queue items (scheduled or publishing - future dates only for queue)
+ const queueItems = useMemo(() => {
+ return allContent
+ .filter((c: Content) => c.site_status === 'scheduled' || c.site_status === 'publishing')
+ .sort((a: Content, b: Content) => {
+ const dateA = a.scheduled_publish_at ? new Date(a.scheduled_publish_at).getTime() : 0;
+ const dateB = b.scheduled_publish_at ? new Date(b.scheduled_publish_at).getTime() : 0;
+ return dateA - dateB;
+ });
+ }, [allContent]);
+
+ // Derived state: Published items (site_status = 'published')
+ const publishedItems = useMemo(() => {
+ return allContent.filter((c: Content) => c.site_status === 'published');
+ }, [allContent]);
+
+ // Derived state: Approved items for sidebar (approved but not scheduled/publishing/published)
+ const approvedItems = useMemo(() => {
+ return allContent.filter(
+ (c: Content) =>
+ c.status === 'approved' &&
+ c.site_status !== 'scheduled' &&
+ c.site_status !== 'publishing' &&
+ c.site_status !== 'published'
+ );
+ }, [allContent]);
+
+ // Calculate stats from allContent
+ const stats = useMemo(() => {
+ const now = new Date();
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
+ const thirtyDaysFromNow = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
+
+ // Published in last 30 days
+ const publishedLast30Days = allContent.filter((c: Content) => {
+ if (c.site_status !== 'published') return false;
+ const publishDate = c.site_status_updated_at ? new Date(c.site_status_updated_at) : null;
+ return publishDate && publishDate >= thirtyDaysAgo;
+ }).length;
+
+ // Scheduled in next 30 days
+ const scheduledNext30Days = allContent.filter((c: Content) => {
+ if (c.site_status !== 'scheduled') return false;
+ const schedDate = c.scheduled_publish_at ? new Date(c.scheduled_publish_at) : null;
+ return schedDate && schedDate >= now && schedDate <= thirtyDaysFromNow;
+ }).length;
+
+ return {
+ scheduled: allContent.filter((c: Content) => c.site_status === 'scheduled').length,
+ publishing: allContent.filter((c: Content) => c.site_status === 'publishing').length,
+ published: allContent.filter((c: Content) => c.site_status === 'published').length,
+ review: allContent.filter((c: Content) => c.status === 'review').length,
+ approved: allContent.filter((c: Content) => c.status === 'approved' && c.site_status !== 'published').length,
+ publishedLast30Days,
+ scheduledNext30Days,
+ };
+ }, [allContent]);
+
+ const loadQueue = useCallback(async () => {
+ if (!activeSite?.id) return;
+
+ try {
+ setLoading(true);
+
+ // Fetch all content for this site
+ const response = await fetchContent({
+ site_id: activeSite.id,
+ page_size: 200,
+ });
+
+ setAllContent(response.results || []);
+ } catch (error: any) {
+ toast.error(`Failed to load content: ${error.message}`);
+ } finally {
+ setLoading(false);
+ }
+ }, [activeSite?.id, toast]);
+
+ useEffect(() => {
+ if (activeSite?.id) {
+ loadQueue();
+ }
+ }, [activeSite?.id, loadQueue]);
+
+ // Drag and drop handlers for list view
+ const handleDragStart = (e: React.DragEvent, item: Content, source: 'queue' | 'approved') => {
+ setDraggedItem(item);
+ e.dataTransfer.effectAllowed = 'move';
+ e.dataTransfer.setData('source', source);
+ };
+
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'move';
+ };
+
+ const handleDropOnList = async (e: React.DragEvent, targetItem: Content) => {
+ e.preventDefault();
+ if (!draggedItem || draggedItem.id === targetItem.id) return;
+
+ // If from approved, schedule it
+ const fromApproved = approvedItems.some(i => i.id === draggedItem.id);
+
+ if (fromApproved) {
+ try {
+ // Schedule for tomorrow by default when dropping on list
+ const tomorrow = new Date();
+ tomorrow.setDate(tomorrow.getDate() + 1);
+ tomorrow.setHours(9, 0, 0, 0);
+
+ await scheduleContent(draggedItem.id, tomorrow.toISOString());
+ toast.success(`Scheduled for ${tomorrow.toLocaleDateString()}`);
+ loadQueue(); // Reload to get updated data
+ } catch (error: any) {
+ toast.error(`Failed to schedule: ${error.message}`);
+ }
+ }
+
+ setDraggedItem(null);
+ };
+
+ // Calendar drag and drop
+ const handleDropOnCalendarDate = async (e: React.DragEvent, date: Date) => {
+ e.preventDefault();
+ if (!draggedItem) return;
+
+ // Set the scheduled time to 9 AM on the target date
+ const newDate = new Date(date);
+ newDate.setHours(9, 0, 0, 0);
+
+ try {
+ await scheduleContent(draggedItem.id, newDate.toISOString());
+ toast.success(`Scheduled for ${newDate.toLocaleDateString()}`);
+ loadQueue(); // Reload to get updated data from server
+ } catch (error: any) {
+ toast.error(`Failed to schedule: ${error.message}`);
+ }
+
+ setDraggedItem(null);
+ };
+
+ const handleDragEnd = () => {
+ setDraggedItem(null);
+ };
+
+ const handleRemoveFromQueue = async (item: Content) => {
+ try {
+ await unscheduleContent(item.id);
+ toast.success('Removed from queue');
+ loadQueue(); // Reload to get updated data
+ } catch (error: any) {
+ toast.error(`Failed to remove: ${error.message}`);
+ }
+ };
+
+ const handleViewContent = (item: Content) => {
+ navigate(`/writer/content/${item.id}`);
+ };
+
+ const formatScheduledTime = (dateStr: string | null | undefined) => {
+ if (!dateStr) return 'Not scheduled';
+ const date = new Date(dateStr);
+ return date.toLocaleString('en-US', {
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: '2-digit',
+ hour12: true,
+ });
+ };
+
+ const getStatusBadge = (item: Content) => {
+ if (item.site_status === 'publishing') {
+ return (
+
+
+ Publishing...
+
+ );
+ }
+ return (
+
+
+ Scheduled
+
+ );
+ };
+
+ // Calendar view helpers
+ const getCalendarDays = () => {
+ const today = new Date();
+ const days = [];
+ // Start from beginning of current week
+ const startOfWeek = new Date(today);
+ startOfWeek.setDate(today.getDate() - today.getDay());
+
+ // Show 4 weeks
+ for (let i = 0; i < 28; i++) {
+ const date = new Date(startOfWeek);
+ date.setDate(startOfWeek.getDate() + i);
+ days.push(date);
+ }
+ return days;
+ };
+
+ // Get scheduled items for a specific date
+ const getScheduledItemsForDate = (date: Date) => {
+ return queueItems.filter(item => {
+ if (!item.scheduled_publish_at) return false;
+ const itemDate = new Date(item.scheduled_publish_at);
+ return (
+ itemDate.getDate() === date.getDate() &&
+ itemDate.getMonth() === date.getMonth() &&
+ itemDate.getFullYear() === date.getFullYear()
+ );
+ });
+ };
+
+ // Get published items for a specific date
+ const getPublishedItemsForDate = (date: Date) => {
+ return publishedItems.filter(item => {
+ // Use site_status_updated_at as publish date
+ const publishDate = item.site_status_updated_at || item.updated_at;
+ if (!publishDate) return false;
+ const itemDate = new Date(publishDate);
+ return (
+ itemDate.getDate() === date.getDate() &&
+ itemDate.getMonth() === date.getMonth() &&
+ itemDate.getFullYear() === date.getFullYear()
+ );
+ });
+ };
+
+ if (!activeSite) {
+ return (
+
+
+
+
+
Please select a site from the header to view the content calendar
+
+
+ );
+ }
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ {/* Header - Site selector is in app header */}
+
+
, color: 'amber' }}
+ hideSiteSector
+ />
+
+
+ {/* Stats Overview - New layout with count on right, bigger labels, descriptions */}
+
+ {/* Review */}
+
navigate('/writer/review')}
+ >
+
+
+
+
Content awaiting review before approval
+
+
{stats.review}
+
+
+
+ {/* Approved */}
+
navigate('/writer/approved')}
+ >
+
+
+
+
Ready to be scheduled for publishing
+
+
{stats.approved}
+
+
+
+ {/* Published */}
+
+
+
+
+
Successfully published to your site
+
+
{stats.published}
+
+
+
+ {/* Scheduled */}
+
+
+
+
+
Queued for automatic publishing
+
+
{stats.scheduled}
+
+
+
+
+ {/* 30-day summary */}
+
+
+
+ {stats.publishedLast30Days} published in last 30 days
+
+
+ {stats.scheduledNext30Days} scheduled for next 30 days
+
+
+
+ setViewMode('calendar')}
+ startIcon={}
+ >
+ Calendar
+
+ setViewMode('list')}
+ startIcon={}
+ >
+ List
+
+
+
+
+ {/* Main Content Area with Sidebar */}
+
+ {/* Main Calendar/List View */}
+
+ {queueItems.length === 0 && approvedItems.length === 0 ? (
+
+
+
+ No content to schedule
+
+
+ Approve content from the review queue to schedule for publishing.
+
+
+
+ ) : viewMode === 'list' ? (
+ /* List View */
+
+ {
+ e.preventDefault();
+ if (!draggedItem) return;
+ // If dropping on empty area, schedule for tomorrow
+ const fromApproved = approvedItems.some(i => i.id === draggedItem.id);
+ if (fromApproved) {
+ const tomorrow = new Date();
+ tomorrow.setDate(tomorrow.getDate() + 1);
+ tomorrow.setHours(9, 0, 0, 0);
+ scheduleContent(draggedItem.id, tomorrow.toISOString())
+ .then(() => {
+ toast.success(`Scheduled for ${tomorrow.toLocaleDateString()}`);
+ loadQueue();
+ })
+ .catch((err) => toast.error(`Failed to schedule: ${err.message}`));
+ }
+ setDraggedItem(null);
+ }}
+ >
+ {queueItems.length === 0 ? (
+
+
Drop approved content here to schedule
+
+ ) : (
+ queueItems.map((item, index) => (
+
handleDragStart(e, item, 'queue')}
+ onDragOver={handleDragOver}
+ onDrop={(e) => handleDropOnList(e, item)}
+ onDragEnd={handleDragEnd}
+ className={`
+ flex items-center gap-4 p-4 bg-white dark:bg-gray-800 rounded-lg border-2
+ ${draggedItem?.id === item.id ? 'border-brand-500 opacity-50' : 'border-gray-200 dark:border-gray-700'}
+ hover:border-brand-300 dark:hover:border-brand-700 transition-all cursor-move
+ `}
+ >
+
+ {index + 1}
+
+
+
+ {item.title}
+
+
+
+
+ {formatScheduledTime(item.scheduled_publish_at)}
+
+
+
+ {getStatusBadge(item)}
+
+ }
+ variant="ghost"
+ tone="neutral"
+ size="sm"
+ onClick={() => handleViewContent(item)}
+ title="View content"
+ />
+ }
+ variant="ghost"
+ tone="danger"
+ size="sm"
+ onClick={() => handleRemoveFromQueue(item)}
+ title="Remove from queue"
+ />
+
+
+ ))
+ )}
+
+
+ ) : (
+ /* Calendar View with drag-drop */
+
+
+ {/* Day headers */}
+ {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
+
+ {day}
+
+ ))}
+
+ {/* Calendar days */}
+ {getCalendarDays().map((date, index) => {
+ const scheduledItems = getScheduledItemsForDate(date);
+ const publishedOnDate = getPublishedItemsForDate(date);
+ const isToday = date.toDateString() === new Date().toDateString();
+ const isPast = date < new Date(new Date().setHours(0, 0, 0, 0));
+
+ return (
+
handleDropOnCalendarDate(e, date) : undefined}
+ className={`
+ min-h-[100px] p-2 rounded-lg border transition-colors
+ ${isToday
+ ? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20'
+ : isPast
+ ? 'border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-900/30'
+ : 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800'
+ }
+ ${!isPast && draggedItem ? 'border-dashed border-brand-300 dark:border-brand-600' : ''}
+ `}
+ >
+
+ {date.getDate()}
+ {date.getDate() === 1 && (
+
+ {date.toLocaleString('default', { month: 'short' })}
+
+ )}
+
+
+ {/* Published items with glass effect */}
+ {publishedOnDate.slice(0, 2).map(item => (
+
+ handleViewContent(item)}
+ className="text-xs p-1.5 bg-success-100/60 dark:bg-success-900/20 text-success-700 dark:text-success-300 rounded truncate cursor-pointer backdrop-blur-sm border border-success-200/50 dark:border-success-800/50"
+ >
+ ✓ {item.title}
+
+
+ ))}
+ {/* Scheduled items */}
+ {scheduledItems.slice(0, 3 - publishedOnDate.length).map(item => (
+
+ handleDragStart(e, item, 'queue') : undefined}
+ onDragEnd={handleDragEnd}
+ onClick={() => handleViewContent(item)}
+ className={`text-xs p-1.5 rounded truncate transition-colors ${
+ isPast
+ ? 'bg-gray-100/60 dark:bg-gray-800/40 text-gray-500 dark:text-gray-400 backdrop-blur-sm cursor-default'
+ : 'bg-warning-100 dark:bg-warning-900/30 text-warning-800 dark:text-warning-200 cursor-move hover:bg-warning-200 dark:hover:bg-warning-900/50'
+ }`}
+ >
+ {item.title}
+
+
+ ))}
+ {(scheduledItems.length + publishedOnDate.length) > 3 && (
+
+ +{(scheduledItems.length + publishedOnDate.length) - 3} more
+
+ )}
+
+
+ );
+ })}
+
+
+ )}
+
+
+ {/* Approved Content Sidebar - reduced width by 15% (80 -> 68) */}
+
+
+
+ {approvedItems.length === 0 ? (
+
+
+
No approved content
+
+
+ ) : (
+ approvedItems.map(item => (
+
handleDragStart(e, item, 'approved')}
+ onDragEnd={handleDragEnd}
+ className={`
+ p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700
+ ${draggedItem?.id === item.id ? 'opacity-50 border-brand-500' : ''}
+ hover:border-success-300 dark:hover:border-success-700 transition-colors cursor-move
+ `}
+ >
+
+ {item.title}
+
+
+
+
+ Approved
+
+
+
+ ))
+ )}
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/Sites/Dashboard.tsx b/frontend/src/pages/Sites/Dashboard.tsx
index 9e107bb4..b18955c5 100644
--- a/frontend/src/pages/Sites/Dashboard.tsx
+++ b/frontend/src/pages/Sites/Dashboard.tsx
@@ -344,7 +344,7 @@ export default function SiteDashboard() {
-
Publishing Queue
-
View scheduled content
+
Content Calendar
+
Schedule and manage content publishing
diff --git a/frontend/src/pages/Sites/Settings.tsx b/frontend/src/pages/Sites/Settings.tsx
index a74f7951..3fddec2b 100644
--- a/frontend/src/pages/Sites/Settings.tsx
+++ b/frontend/src/pages/Sites/Settings.tsx
@@ -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
(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(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 = {
+ 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([]);
const [selectedIndustry, setSelectedIndustry] = useState('');
@@ -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 */}
-
+
+
+
+ {/* Content Generation Tab */}
+ {activeTab === 'content-generation' && (
+
+
+
+
+
+
+
+
Content Generation
+
Customize how your articles are written
+
+
+
+ {contentGenerationLoading ? (
+
+
+
+ ) : (
+
+
+
+
+
+
+
+
+ setContentGenerationSettings({ ...contentGenerationSettings, defaultTone: value })}
+ className="w-full"
+ />
+
+
+
+
+ setContentGenerationSettings({ ...contentGenerationSettings, defaultLength: value })}
+ className="w-full"
+ />
+
+
+
+ )}
+
+
+
+ : }
+ >
+ {contentGenerationSaving ? 'Saving...' : 'Save Settings'}
+
+
+
+ )}
+
+ {/* Image Settings Tab */}
+ {activeTab === 'image-settings' && (
+
+
+
+
+
+
+
+
Image Generation
+
Configure how images are created for your articles
+
+
+
+ {imageSettingsLoading ? (
+
+
+
+ ) : (
+
+ {/* Row 1: Image Quality & Style */}
+
+
+
+
setImageQuality(value as 'standard' | 'premium' | 'best')}
+ className="w-full"
+ />
+ Higher quality produces better images
+
+
+
+
+
setImageSettings({ ...imageSettings, image_type: value as any })}
+ className="w-full"
+ />
+ Choose the visual style that matches your brand
+
+
+
+ {/* Row 2: Featured Image Size */}
+
+
+
+
+
Featured Image Size
+
Always Enabled
+
+
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"
+ />
+
+
+
+ {/* Row 3: Desktop & Mobile Images */}
+
+
+
+ setImageSettings({ ...imageSettings, desktop_enabled: checked })}
+ />
+
+
+ {imageSettings.desktop_enabled && (
+
setImageSettings({ ...imageSettings, desktop_image_size: value })}
+ className="w-full"
+ />
+ )}
+
+
+
+
setImageSettings({ ...imageSettings, mobile_enabled: checked })}
+ />
+
+
+
512×512 pixels
+
+
+
+
+ {/* Row 4: Max Images & Format */}
+
+
+
+ setImageSettings({ ...imageSettings, max_in_article_images: parseInt(value) })}
+ className="w-full"
+ />
+
+
+
+
+ setImageSettings({ ...imageSettings, image_format: value as any })}
+ className="w-full"
+ />
+
+
+
+ )}
+
+
+
+ : }
+ >
+ {imageSettingsSaving ? 'Saving...' : 'Save Settings'}
+
+
+
+ )}
+
{/* Publishing Tab */}
{activeTab === 'publishing' && (
diff --git a/frontend/src/styles/design-system.css b/frontend/src/styles/design-system.css
index 6e11a769..b98eed7a 100644
--- a/frontend/src/styles/design-system.css
+++ b/frontend/src/styles/design-system.css
@@ -460,7 +460,7 @@
----------------------------------------------------------------- */
@utility menu-item {
- @apply relative flex items-center w-full gap-3.5 px-4 py-3 font-medium rounded-lg text-theme-sm;
+ @apply relative flex items-center w-full gap-3 px-3 py-1.5 font-medium rounded-lg text-theme-sm;
}
@utility menu-item-active {
@@ -472,7 +472,7 @@
}
@utility menu-item-icon-size {
- @apply w-6 h-6 flex-shrink-0;
+ @apply w-5 h-5 flex-shrink-0;
& svg { width: 100%; height: 100%; }
}
@@ -489,7 +489,7 @@
----------------------------------------------------------------- */
@utility menu-dropdown-item {
- @apply block px-3.5 py-2.5 text-theme-sm font-medium rounded-md transition-colors;
+ @apply block px-3 py-1.5 text-theme-sm font-medium rounded-md transition-colors;
}
@utility menu-dropdown-item-active {