+
{/* Trigger Button - styled like igny8-select-styled */}
diff --git a/frontend/src/layout/AppSidebar.tsx b/frontend/src/layout/AppSidebar.tsx
index 65eef128..dfc1aaf2 100644
--- a/frontend/src/layout/AppSidebar.tsx
+++ b/frontend/src/layout/AppSidebar.tsx
@@ -159,7 +159,6 @@ const AppSidebar: React.FC = () => {
subItems: [
{ name: "Content Review", path: "/writer/review" },
{ name: "Publish / Schedule", path: "/writer/approved" },
- { name: "Publish Settings", path: "/publisher/settings" },
{ name: "Content Calendar", path: "/publisher/content-calendar" },
],
});
diff --git a/frontend/src/pages/Sites/AIAutomationSettings.tsx b/frontend/src/pages/Sites/AIAutomationSettings.tsx
new file mode 100644
index 00000000..a7749a68
--- /dev/null
+++ b/frontend/src/pages/Sites/AIAutomationSettings.tsx
@@ -0,0 +1,807 @@
+/**
+ * AI & Automation Settings Component
+ * Per SETTINGS-CONSOLIDATION-PLAN.md
+ *
+ * Unified settings page for site automation, stage configuration, and publishing schedule.
+ * Location: Site Settings > Automation tab
+ */
+import React, { useState, useEffect, useCallback } from 'react';
+import { Card } from '../../components/ui/card';
+import Button from '../../components/ui/button/Button';
+import IconButton from '../../components/ui/button/IconButton';
+import Label from '../../components/form/Label';
+import InputField from '../../components/form/input/InputField';
+import SelectDropdown from '../../components/form/SelectDropdown';
+import Switch from '../../components/form/switch/Switch';
+import Checkbox from '../../components/form/input/Checkbox';
+import { useToast } from '../../components/ui/toast/ToastContainer';
+import Badge from '../../components/ui/badge/Badge';
+import { fetchAPI } from '../../services/api';
+import {
+ BoltIcon,
+ CalendarIcon,
+ Loader2Icon,
+ SaveIcon,
+ ClockIcon,
+ PlayIcon,
+ InfoIcon,
+ CloseIcon,
+ PlusIcon,
+ ImageIcon,
+} from '../../icons';
+import {
+ getUnifiedSiteSettings,
+ updateUnifiedSiteSettings,
+ UnifiedSiteSettings,
+ StageConfig,
+ DAYS_OF_WEEK,
+ FREQUENCY_OPTIONS,
+ calculateTotalBudget,
+} from '../../services/unifiedSettings.api';
+
+interface AIAutomationSettingsProps {
+ siteId: number;
+}
+
+// Tooltip component for inline help
+function Tooltip({ text, children }: { text: string; children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ {text}
+
+
+ );
+}
+
+// Image settings types
+interface ImageStyle {
+ value: string;
+ label: string;
+}
+
+export default function AIAutomationSettings({ siteId }: AIAutomationSettingsProps) {
+ const toast = useToast();
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [settings, setSettings] = useState
(null);
+
+ // Image generation settings (from tenant-wide AI settings)
+ const [imageSettingsLoading, setImageSettingsLoading] = useState(true);
+ const [availableStyles, setAvailableStyles] = useState([
+ { value: 'photorealistic', label: 'Photorealistic' },
+ { value: 'illustration', label: 'Illustration' },
+ { value: '3d_render', label: '3D Render' },
+ { value: 'minimal_flat', label: 'Minimal / Flat' },
+ { value: 'artistic', label: 'Artistic' },
+ { value: 'cartoon', label: 'Cartoon' },
+ ]);
+ const [selectedStyle, setSelectedStyle] = useState('photorealistic');
+ const [maxImages, setMaxImages] = useState(4);
+ const [maxAllowed, setMaxAllowed] = useState(8);
+ const [featuredImageSize, setFeaturedImageSize] = useState('2560x1440');
+ const [landscapeImageSize, setLandscapeImageSize] = useState('2560x1440');
+ const [squareImageSize, setSquareImageSize] = useState('2048x2048');
+
+ // Load unified settings
+ const loadSettings = useCallback(async () => {
+ try {
+ setLoading(true);
+ const data = await getUnifiedSiteSettings(siteId);
+ setSettings(data);
+ } catch (error) {
+ console.error('Failed to load unified settings:', error);
+ toast.error(`Failed to load settings: ${(error as Error).message}`);
+ } finally {
+ setLoading(false);
+ }
+ }, [siteId, toast]);
+
+ // Load image settings from tenant-wide AI settings API
+ const loadImageSettings = useCallback(async () => {
+ try {
+ setImageSettingsLoading(true);
+ const response = await fetchAPI('/v1/account/settings/ai/');
+ if (response?.image_generation) {
+ if (response.image_generation.styles) {
+ setAvailableStyles(response.image_generation.styles);
+ }
+ setSelectedStyle(response.image_generation.selected_style || 'photorealistic');
+ setMaxImages(response.image_generation.max_images ?? 4);
+ setMaxAllowed(response.image_generation.max_allowed ?? 8);
+ setFeaturedImageSize(response.image_generation.featured_image_size || '2560x1440');
+ setLandscapeImageSize(response.image_generation.landscape_image_size || '2560x1440');
+ setSquareImageSize(response.image_generation.square_image_size || '2048x2048');
+ }
+ } catch (error) {
+ console.error('Failed to load image settings:', error);
+ } finally {
+ setImageSettingsLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ loadSettings();
+ loadImageSettings();
+ }, [loadSettings, loadImageSettings]);
+
+ // Save settings
+ const handleSave = async () => {
+ if (!settings) return;
+
+ try {
+ setSaving(true);
+
+ // Save unified settings
+ const updated = await updateUnifiedSiteSettings(siteId, {
+ automation: settings.automation,
+ stages: settings.stages.map(s => ({
+ number: s.number,
+ enabled: s.enabled,
+ batch_size: s.batch_size,
+ per_run_limit: s.per_run_limit,
+ use_testing: s.use_testing,
+ budget_pct: s.budget_pct,
+ })),
+ delays: settings.delays,
+ publishing: {
+ auto_approval_enabled: settings.publishing.auto_approval_enabled,
+ auto_publish_enabled: settings.publishing.auto_publish_enabled,
+ publish_days: settings.publishing.publish_days,
+ time_slots: settings.publishing.time_slots,
+ },
+ });
+ setSettings(updated);
+
+ // Save image settings
+ await fetchAPI('/v1/account/settings/ai/', {
+ method: 'POST',
+ body: JSON.stringify({
+ image_generation: {
+ image_style: selectedStyle,
+ max_images_per_article: maxImages,
+ },
+ }),
+ });
+
+ toast.success('Settings saved successfully');
+ } catch (error) {
+ console.error('Failed to save settings:', error);
+ toast.error(`Failed to save settings: ${(error as Error).message}`);
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ // Reset to defaults
+ const handleReset = () => {
+ loadSettings();
+ loadImageSettings();
+ toast.info('Settings reset to last saved values');
+ };
+
+ // Update automation settings
+ const updateAutomation = (updates: Partial) => {
+ if (!settings) return;
+ setSettings({
+ ...settings,
+ automation: { ...settings.automation, ...updates },
+ });
+ };
+
+ // Update stage configuration
+ const updateStage = (stageNumber: number, updates: Partial) => {
+ if (!settings) return;
+ setSettings({
+ ...settings,
+ stages: settings.stages.map(s =>
+ s.number === stageNumber ? { ...s, ...updates } : s
+ ),
+ });
+ };
+
+ // Update delays
+ const updateDelays = (updates: Partial) => {
+ if (!settings) return;
+ setSettings({
+ ...settings,
+ delays: { ...settings.delays, ...updates },
+ });
+ };
+
+ // Update publishing settings
+ const updatePublishing = (updates: Partial) => {
+ if (!settings) return;
+ setSettings({
+ ...settings,
+ publishing: { ...settings.publishing, ...updates },
+ });
+ };
+
+ // Toggle day in publish_days
+ const toggleDay = (day: string) => {
+ if (!settings) return;
+ const days = settings.publishing.publish_days;
+ const newDays = days.includes(day)
+ ? days.filter(d => d !== day)
+ : [...days, day];
+ updatePublishing({ publish_days: newDays });
+ };
+
+ // Add time slot
+ const addTimeSlot = () => {
+ if (!settings) return;
+ const slots = settings.publishing.time_slots;
+ let newSlot = '09:00';
+ if (slots.length > 0) {
+ const lastSlot = slots[slots.length - 1];
+ const [hours, mins] = lastSlot.split(':').map(Number);
+ const newHours = (hours + 3) % 24;
+ newSlot = `${String(newHours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
+ }
+ updatePublishing({ time_slots: [...slots, newSlot] });
+ };
+
+ // Remove time slot
+ const removeTimeSlot = (index: number) => {
+ if (!settings) return;
+ const newSlots = settings.publishing.time_slots.filter((_, i) => i !== index);
+ updatePublishing({ time_slots: newSlots });
+ };
+
+ // Update time slot at index
+ const updateTimeSlot = (index: number, newSlot: string) => {
+ if (!settings) return;
+ const slots = [...settings.publishing.time_slots];
+ slots[index] = newSlot;
+ updatePublishing({ time_slots: slots });
+ };
+
+ // Clear all time slots
+ const clearAllSlots = () => {
+ if (!settings) return;
+ updatePublishing({ time_slots: [] });
+ };
+
+ if (loading || imageSettingsLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!settings) {
+ return (
+
+
Failed to load settings. Please try again.
+
+
+ );
+ }
+
+ const totalBudget = calculateTotalBudget(settings.stages);
+ const hasTestingEnabled = settings.stages.some(s => s.has_ai && s.use_testing);
+
+ return (
+
+ {/* Row 1: Three Cards - Automation Schedule, Content Publishing, Image Generation */}
+
+
+ {/* Card 1: Automation Schedule */}
+
+
+
+
+
+
+
Automation
+
Schedule runs
+
+
+
+
+
+
+ updateAutomation({ enabled: checked })}
+ />
+
+
+
+
+
+ ({ value: f.value, label: f.label }))}
+ value={settings.automation.frequency}
+ onChange={(value) => updateAutomation({ frequency: value as 'hourly' | 'daily' | 'weekly' })}
+ disabled={!settings.automation.enabled}
+ className="w-full"
+ />
+
+
+
+ updateAutomation({ time: e.target.value })}
+ disabled={!settings.automation.enabled}
+ />
+
+
+
+
+
+
+ {settings.automation.enabled && settings.automation.next_run_at ? (
+ Next run: {new Date(settings.automation.next_run_at).toLocaleString()}
+ ) : (
+ Runs {settings.automation.frequency} at {settings.automation.time}
+ )}
+
+
+
+
+
+ {/* Card 2: Content Publishing */}
+
+
+
+
+
Publishing
+
Auto-publish options
+
+
+
+
+
+
+ updatePublishing({ auto_approval_enabled: checked })}
+ />
+
+
+
+
+ updatePublishing({ auto_publish_enabled: checked })}
+ />
+
+
+ {/* AI Mode Status */}
+
+
+ AI Mode:
+
+ {hasTestingEnabled ? 'Testing' : 'Live'}
+
+
+
+ {hasTestingEnabled ? 'Using test models' : 'Production models'}
+
+
+
+
+
+ {/* Card 3: Image Generation (Style & Count only) */}
+
+
+
+
+
+
+
Images
+
Style & count
+
+
+
+
+
+
+ ({ value: s.value, label: s.label }))}
+ value={selectedStyle}
+ onChange={(value) => setSelectedStyle(value)}
+ className="w-full"
+ />
+
+
+
+
+ ({
+ value: String(i + 1),
+ label: `${i + 1} image${i > 0 ? 's' : ''}`,
+ }))}
+ value={String(maxImages)}
+ onChange={(value) => setMaxImages(parseInt(value))}
+ className="w-full"
+ />
+
+
+ {/* Image Sizes */}
+
+
+
Featured
+
{featuredImageSize}
+
+
+
Landscape
+
{landscapeImageSize}
+
+
+
Square
+
{squareImageSize}
+
+
+
+
+
+
+ {/* Row 2: Stage Configuration (2/3) + Schedule & Capacity (1/3) */}
+
+
+ {/* Left: Stage Configuration Matrix (2/3 width) */}
+
+
+
+
+
+
+
+
Stage Configuration
+
Configure each automation stage
+
+
+
+ Budget: {totalBudget}%
+
+
+
+ {/* Info Banner */}
+
+
+
+
+ Limit: max items per run (0=all).
+ Budget: credit allocation across AI stages.
+
+
+
+
+ {/* Stage Table */}
+
+
+
+ | Stage |
+
+ On
+ |
+
+ Batch
+ |
+
+ Limit
+ |
+
+ Model
+ |
+
+ Budget
+ |
+
+
+
+ {settings.stages.map((stage) => (
+
+ |
+
+ {stage.number}.
+
+ {stage.name}
+
+ {!stage.has_ai && (local)}
+
+ |
+
+ updateStage(stage.number, { enabled: checked })}
+ />
+ |
+
+ updateStage(stage.number, { batch_size: parseInt(e.target.value) || 1 })}
+ min="1"
+ max="100"
+ disabled={!stage.enabled}
+ className="w-16 text-center"
+ />
+ |
+
+ updateStage(stage.number, { per_run_limit: parseInt(e.target.value) || 0 })}
+ min="0"
+ max="1000"
+ disabled={!stage.enabled}
+ className="w-16 text-center"
+ />
+ |
+
+ {stage.has_ai ? (
+ updateStage(stage.number, { use_testing: value === 'true' })}
+ disabled={!stage.enabled}
+ className="w-full"
+ />
+ ) : (
+ -
+ )}
+ |
+
+ {stage.has_ai ? (
+ updateStage(stage.number, { budget_pct: parseInt(e.target.value) || 0 })}
+ min="0"
+ max="100"
+ disabled={!stage.enabled}
+ className="w-16 text-center"
+ />
+ ) : (
+ -
+ )}
+ |
+
+ ))}
+
+
+
+ {/* Delays Row */}
+
+
+
+
+ updateDelays({ between_stage: parseInt(e.target.value) || 0 })}
+ min="0"
+ max="60"
+ className="w-16 text-center"
+ />
+ sec
+
+
+
+ updateDelays({ within_stage: parseInt(e.target.value) || 0 })}
+ min="0"
+ max="60"
+ className="w-16 text-center"
+ />
+ sec
+
+
+
+
+
+ {/* Right: Schedule + Capacity stacked */}
+
+ {/* Schedule Card */}
+
+
+
+
+
+
+
Schedule
+
Days and time slots
+
+
+
+ {/* Days Selection */}
+
+
+
+ {DAYS_OF_WEEK.map((day) => (
+
+ ))}
+
+
+
+ {/* Time Slots */}
+
+
+
+ {settings.publishing.time_slots.length > 0 && (
+
+ )}
+
+
+
+ {settings.publishing.time_slots.length === 0 ? (
+
+ No time slots. Add at least one.
+
+ ) : (
+ settings.publishing.time_slots.map((slot, index) => (
+
+ #{index + 1}
+ updateTimeSlot(index, e.target.value)}
+ className="w-32"
+ />
+ }
+ variant="ghost"
+ tone="danger"
+ size="sm"
+ title="Remove"
+ onClick={() => removeTimeSlot(index)}
+ />
+
+ ))
+ )}
+
+
}
+ onClick={addTimeSlot}
+ className="mt-3"
+ >
+ Add Slot
+
+
+
+
+ {/* Calculated Capacity Card */}
+
+
+
+
+
+
+
Capacity
+
Calculated output
+
+
+
+
+
+
+ {settings.publishing.daily_capacity}
+
+
Daily
+
+
+
+ {settings.publishing.weekly_capacity}
+
+
Weekly
+
+
+
+ ~{settings.publishing.monthly_capacity}
+
+
Monthly
+
+
+
+
+
+ Based on {settings.publishing.publish_days.length} days × {settings.publishing.time_slots.length} slots
+
+
+
+
+
+ {/* Action Buttons */}
+
+
+ : }
+ >
+ {saving ? 'Saving...' : 'Save Settings'}
+
+
+
+ {/* Help Cards - 3 cards explaining different sections */}
+
+ {/* How Publishing Works */}
+
+
+
+
+
How Publishing Works
+
+ - Content: Draft → Review → Approved → Published
+ - Auto-approval moves Review to Approved
+ - Auto-publish sends to WordPress
+ - Manual publish always available
+
+
+
+
+
+ {/* Stage Configuration Explanation */}
+
+
+
+
+
Stage Configuration
+
+ - Batch: Items processed together
+ - Limit: Max items per automation run
+ - Model: Live (production) or Test mode
+ - Budget: Credit allocation per stage
+
+
+
+
+
+ {/* Schedule & Capacity Explanation */}
+
+
+
+
+
Schedule & Capacity
+
+ - Select days content can be published
+ - Add time slots for publishing
+ - Capacity = Days × Time Slots
+ - Content scheduled to next available slot
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/Sites/Settings.tsx b/frontend/src/pages/Sites/Settings.tsx
index a693fed2..3e0a16cf 100644
--- a/frontend/src/pages/Sites/Settings.tsx
+++ b/frontend/src/pages/Sites/Settings.tsx
@@ -34,6 +34,7 @@ import Badge from '../../components/ui/badge/Badge';
import { Dropdown } from '../../components/ui/dropdown/Dropdown';
import { DropdownItem } from '../../components/ui/dropdown/DropdownItem';
import SiteInfoBar from '../../components/common/SiteInfoBar';
+import AIAutomationSettings from './AIAutomationSettings';
export default function SiteSettings() {
const { id: siteId } = useParams<{ id: string }>();
@@ -51,9 +52,9 @@ export default function SiteSettings() {
const [isSiteSelectorOpen, setIsSiteSelectorOpen] = useState(false);
const siteSelectorRef = useRef(null);
- // Check for tab parameter in URL - content-types removed, redirects to integrations
- const initialTab = (searchParams.get('tab') as 'general' | 'ai-settings' | 'integrations') || 'general';
- const [activeTab, setActiveTab] = useState<'general' | 'ai-settings' | 'integrations'>(initialTab);
+ // Check for tab parameter in URL - ai-settings removed (content in Automation tab)
+ const initialTab = (searchParams.get('tab') as 'general' | 'automation' | 'integrations') || 'general';
+ const [activeTab, setActiveTab] = useState<'general' | 'automation' | 'integrations'>(initialTab);
// Advanced Settings toggle
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
@@ -137,7 +138,7 @@ export default function SiteSettings() {
useEffect(() => {
// Update tab if URL parameter changes
const tab = searchParams.get('tab');
- if (tab && ['general', 'ai-settings', 'integrations'].includes(tab)) {
+ if (tab && ['general', 'ai-settings', 'automation', 'integrations'].includes(tab)) {
setActiveTab(tab as typeof activeTab);
}
// Handle legacy tab names - redirect content-types to integrations
@@ -580,17 +581,17 @@ export default function SiteSettings() {
- {/* AI Settings Tab (merged content-generation + image-settings) */}
- {activeTab === 'ai-settings' && (
-
- {/* 3 Cards in a Row */}
-
-
- {/* Card 1: Content Settings */}
-
-
-
-
-
-
-
Content Settings
-
Customize article writing
-
-
-
- {contentGenerationLoading ? (
-
-
-
- ) : (
-
-
-
-
-
- setContentGenerationSettings({ ...contentGenerationSettings, defaultTone: value })}
- className="w-full"
- />
-
-
-
-
- setContentGenerationSettings({ ...contentGenerationSettings, defaultLength: value })}
- className="w-full"
- />
-
-
- )}
-
-
- {/* Card 2: AI Parameters */}
-
-
-
-
-
-
-
AI Parameters
-
Fine-tune content generation behavior
-
-
-
- {aiSettingsLoading ? (
-
-
-
- ) : (
-
- {/* Temperature Slider */}
-
-
-
-
-
setTemperature(parseFloat(e.target.value))}
- className="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-success-500"
- />
-
- More focused
- More creative
-
-
-
{temperature.toFixed(1)}
-
-
-
- {/* Max Tokens Dropdown */}
-
-
-
setMaxTokens(parseInt(value))}
- className="w-full"
- />
-
- Maximum length of generated content. Higher values allow longer articles.
-
-
-
- )}
-
-
- {/* Card 3: Image Generation */}
-
-
-
-
-
-
-
Image Generation
-
Quality & style
-
-
-
- {aiSettingsLoading ? (
-
-
-
- ) : (
-
- {/* Quality Tier Dropdown */}
-
-
- 0
- ? qualityTiers.map(tier => ({
- value: tier.tier || tier.value,
- label: `${tier.label} (${tier.credits} credits)`
- }))
- : [
- { value: 'basic', label: 'Basic (1 credit)' },
- { value: 'quality', label: 'Quality (5 credits)' },
- { value: 'premium', label: 'Premium (15 credits)' },
- ]
- }
- value={selectedTier || 'quality'}
- onChange={(value) => setSelectedTier(value)}
- className="w-full"
- />
-
-
- {/* Image Style Dropdown */}
-
-
- 0
- ? availableStyles.map(style => ({ value: style.value, label: style.label }))
- : [
- { value: 'photorealistic', label: 'Photorealistic' },
- { value: 'illustration', label: 'Illustration' },
- { value: '3d_render', label: '3D Render' },
- { value: 'minimal_flat', label: 'Minimal / Flat' },
- { value: 'artistic', label: 'Artistic' },
- { value: 'cartoon', label: 'Cartoon' },
- ]
- }
- value={selectedStyle}
- onChange={(value) => setSelectedStyle(value)}
- className="w-full"
- />
-
-
- {/* Images Per Article Dropdown */}
-
-
- ({
- value: String(i + 1),
- label: `${i + 1} image${i > 0 ? 's' : ''}`,
- }))}
- value={String(maxImages || 4)}
- onChange={(value) => setMaxImages(parseInt(value))}
- className="w-full"
- />
-
-
- {/* Image Sizes Display */}
-
-
-
Featured Image
-
{featuredImageSize}
-
-
-
Landscape
-
{landscapeImageSize}
-
-
-
Square
-
{squareImageSize}
-
-
-
- )}
-
-
-
- {/* End of 3-card grid */}
-
- {/* Save Button */}
-
-
-
-
+ {/* Automation Tab - Unified AI & Automation Settings */}
+ {activeTab === 'automation' && siteId && (
+
)}
+ {/*
+ AI Settings Tab - REMOVED (Jan 2026)
+ Reason: Text AI override options removed from user control.
+ Image settings moved to Automation tab.
+ Content generation settings (tone, length, append to prompt) to be removed in future.
+ State and functions kept for backward compatibility but tab hidden from UI.
+ */}
+
{/* Tab content */}
{/* General Tab */}
diff --git a/frontend/src/services/automationService.ts b/frontend/src/services/automationService.ts
index cfa0fb2e..80232e99 100644
--- a/frontend/src/services/automationService.ts
+++ b/frontend/src/services/automationService.ts
@@ -22,6 +22,15 @@ export interface AutomationConfig {
stage_6_batch_size: number;
within_stage_delay: number;
between_stage_delay: number;
+ // Per-run limits (0 = unlimited)
+ max_keywords_per_run: number;
+ max_clusters_per_run: number;
+ max_ideas_per_run: number;
+ max_tasks_per_run: number;
+ max_content_per_run: number;
+ max_images_per_run: number;
+ max_approvals_per_run: number;
+ max_credits_per_run: number;
last_run_at: string | null;
next_run_at: string | null;
}
diff --git a/frontend/src/services/unifiedSettings.api.ts b/frontend/src/services/unifiedSettings.api.ts
new file mode 100644
index 00000000..1be0ad17
--- /dev/null
+++ b/frontend/src/services/unifiedSettings.api.ts
@@ -0,0 +1,162 @@
+/**
+ * Unified Settings API Service
+ * Per SETTINGS-CONSOLIDATION-PLAN.md
+ *
+ * Consolidates AI & Automation settings into a single API endpoint.
+ */
+import { fetchAPI } from './api';
+
+// ═══════════════════════════════════════════════════════════
+// TYPES
+// ═══════════════════════════════════════════════════════════
+
+export interface StageConfig {
+ number: number;
+ name: string;
+ has_ai: boolean;
+ enabled: boolean;
+ batch_size: number;
+ per_run_limit: number;
+ use_testing?: boolean;
+ budget_pct?: number;
+}
+
+export interface AvailableModel {
+ id: number | null;
+ name: string | null;
+ model_name: string | null;
+}
+
+export interface UnifiedSiteSettings {
+ site_id: number;
+ site_name: string;
+ automation: {
+ enabled: boolean;
+ frequency: 'hourly' | 'daily' | 'weekly';
+ time: string; // HH:MM format
+ last_run_at: string | null;
+ next_run_at: string | null;
+ };
+ stages: StageConfig[];
+ delays: {
+ within_stage: number;
+ between_stage: number;
+ };
+ publishing: {
+ auto_approval_enabled: boolean;
+ auto_publish_enabled: boolean;
+ publish_days: string[]; // ['mon', 'tue', ...]
+ time_slots: string[]; // ['09:00', '14:00', ...]
+ daily_capacity: number;
+ weekly_capacity: number;
+ monthly_capacity: number;
+ };
+ available_models: {
+ text: {
+ testing: AvailableModel | null;
+ live: AvailableModel | null;
+ };
+ image: {
+ testing: AvailableModel | null;
+ live: AvailableModel | null;
+ };
+ };
+}
+
+export interface UpdateUnifiedSettingsRequest {
+ automation?: {
+ enabled?: boolean;
+ frequency?: 'hourly' | 'daily' | 'weekly';
+ time?: string;
+ };
+ stages?: Array<{
+ number: number;
+ enabled?: boolean;
+ batch_size?: number;
+ per_run_limit?: number;
+ use_testing?: boolean;
+ budget_pct?: number;
+ }>;
+ delays?: {
+ within_stage?: number;
+ between_stage?: number;
+ };
+ publishing?: {
+ auto_approval_enabled?: boolean;
+ auto_publish_enabled?: boolean;
+ publish_days?: string[];
+ time_slots?: string[];
+ };
+}
+
+// ═══════════════════════════════════════════════════════════
+// API FUNCTIONS
+// ═══════════════════════════════════════════════════════════
+
+/**
+ * Get unified site settings (AI & Automation consolidated)
+ */
+export async function getUnifiedSiteSettings(siteId: number): Promise {
+ const response = await fetchAPI(`/v1/integration/sites/${siteId}/unified-settings/`);
+ return response.data || response;
+}
+
+/**
+ * Update unified site settings
+ */
+export async function updateUnifiedSiteSettings(
+ siteId: number,
+ data: UpdateUnifiedSettingsRequest
+): Promise {
+ const response = await fetchAPI(`/v1/integration/sites/${siteId}/unified-settings/`, {
+ method: 'PUT',
+ body: JSON.stringify(data),
+ });
+ return response.data || response;
+}
+
+// ═══════════════════════════════════════════════════════════
+// HELPER FUNCTIONS
+// ═══════════════════════════════════════════════════════════
+
+/**
+ * Days of week for publishing schedule
+ */
+export const DAYS_OF_WEEK = [
+ { value: 'mon', label: 'Mon' },
+ { value: 'tue', label: 'Tue' },
+ { value: 'wed', label: 'Wed' },
+ { value: 'thu', label: 'Thu' },
+ { value: 'fri', label: 'Fri' },
+ { value: 'sat', label: 'Sat' },
+ { value: 'sun', label: 'Sun' },
+];
+
+/**
+ * Frequency options for automation
+ */
+export const FREQUENCY_OPTIONS = [
+ { value: 'hourly', label: 'Hourly' },
+ { value: 'daily', label: 'Daily' },
+ { value: 'weekly', label: 'Weekly' },
+];
+
+/**
+ * Format time for display
+ */
+export function formatTime(time: string): string {
+ const [hours, minutes] = time.split(':');
+ const h = parseInt(hours);
+ const ampm = h >= 12 ? 'PM' : 'AM';
+ const displayHour = h > 12 ? h - 12 : h === 0 ? 12 : h;
+ return `${displayHour}:${minutes} ${ampm}`;
+}
+
+/**
+ * Calculate total budget percentage from stages
+ */
+export function calculateTotalBudget(stages: StageConfig[]): number {
+ return stages
+ .filter(s => s.has_ai && s.budget_pct)
+ .reduce((sum, s) => sum + (s.budget_pct || 0), 0);
+}