final polish phase 1
This commit is contained in:
@@ -19,6 +19,7 @@ import ConfigModal from '../../components/Automation/ConfigModal';
|
||||
import RunHistory from '../../components/Automation/RunHistory';
|
||||
import CurrentProcessingCard from '../../components/Automation/CurrentProcessingCard';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ComponentCard from '../../components/common/ComponentCard';
|
||||
import EnhancedMetricCard from '../../components/dashboard/EnhancedMetricCard';
|
||||
import DebugSiteSelector from '../../components/common/DebugSiteSelector';
|
||||
@@ -379,49 +380,34 @@ const AutomationPage: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Content Automation | IGNY8" description="Automatically create and publish content on your schedule" />
|
||||
<PageHeader
|
||||
title="Automation"
|
||||
description="Automatically create and publish content on your schedule"
|
||||
badge={{ icon: <BoltIcon />, color: 'teal' }}
|
||||
parent="Automation"
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="relative flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-center size-10 rounded-xl bg-gradient-to-br from-teal-500 to-teal-600">
|
||||
<BoltIcon className="text-white size-5" />
|
||||
{/* Compact Ready-to-Run card (header) - absolutely centered in header */}
|
||||
<div className="flex justify-center">
|
||||
<div className={`w-full max-w-sm rounded-lg border-2 p-2 transition-all flex items-center gap-3 shadow-sm
|
||||
${currentRun?.status === 'running' ? 'border-blue-500 bg-blue-50' : currentRun?.status === 'paused' ? 'border-amber-500 bg-amber-50' : totalPending > 0 ? 'border-success-500 bg-success-50' : 'border-slate-300 bg-slate-50'}`}>
|
||||
<div className={`size-9 rounded-lg flex items-center justify-center flex-shrink-0
|
||||
${currentRun?.status === 'running' ? 'bg-gradient-to-br from-blue-500 to-blue-600' : currentRun?.status === 'paused' ? 'bg-gradient-to-br from-amber-500 to-amber-600' : totalPending > 0 ? 'bg-gradient-to-br from-success-500 to-success-600' : 'bg-gradient-to-br from-slate-400 to-slate-500'}`}>
|
||||
{!currentRun && totalPending > 0 ? <CheckCircleIcon className="size-4 text-white" /> : currentRun?.status === 'running' ? <BoltIcon className="size-4 text-white" /> : currentRun?.status === 'paused' ? <ClockIcon className="size-4 text-white" /> : <BoltIcon className="size-4 text-white" />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-white truncate">
|
||||
{currentRun?.status === 'running' && `Running - Stage ${currentRun.current_stage}/7`}
|
||||
{currentRun?.status === 'paused' && 'Paused'}
|
||||
{!currentRun && totalPending > 0 && 'Ready to Run'}
|
||||
{!currentRun && totalPending === 0 && 'No Items Pending'}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-800 dark:text-white/90">Automation</h2>
|
||||
{activeSite && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
Site: <span className="font-medium text-brand-600 dark:text-brand-400">{activeSite.name}</span>
|
||||
</p>
|
||||
)}
|
||||
<div className="text-xs text-slate-600 dark:text-gray-400 truncate">
|
||||
{currentRun ? `Started: ${new Date(currentRun.started_at).toLocaleTimeString()}` : (totalPending > 0 ? `${totalPending} items in pipeline` : 'All stages clear')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Compact Ready-to-Run card (header) - absolutely centered in header */}
|
||||
<div className="hidden sm:flex absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 z-10">
|
||||
<div className={`w-full max-w-sm rounded-lg border-2 p-2 transition-all flex items-center gap-3 shadow-sm
|
||||
${currentRun?.status === 'running' ? 'border-blue-500 bg-blue-50' : currentRun?.status === 'paused' ? 'border-amber-500 bg-amber-50' : totalPending > 0 ? 'border-success-500 bg-success-50' : 'border-slate-300 bg-slate-50'}`}>
|
||||
<div className={`size-9 rounded-lg flex items-center justify-center flex-shrink-0
|
||||
${currentRun?.status === 'running' ? 'bg-gradient-to-br from-blue-500 to-blue-600' : currentRun?.status === 'paused' ? 'bg-gradient-to-br from-amber-500 to-amber-600' : totalPending > 0 ? 'bg-gradient-to-br from-success-500 to-success-600' : 'bg-gradient-to-br from-slate-400 to-slate-500'}`}>
|
||||
{!currentRun && totalPending > 0 ? <CheckCircleIcon className="size-4 text-white" /> : currentRun?.status === 'running' ? <BoltIcon className="size-4 text-white" /> : currentRun?.status === 'paused' ? <ClockIcon className="size-4 text-white" /> : <BoltIcon className="size-4 text-white" />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-white truncate">
|
||||
{currentRun?.status === 'running' && `Running - Stage ${currentRun.current_stage}/7`}
|
||||
{currentRun?.status === 'paused' && 'Paused'}
|
||||
{!currentRun && totalPending > 0 && 'Ready to Run'}
|
||||
{!currentRun && totalPending === 0 && 'No Items Pending'}
|
||||
</div>
|
||||
<div className="text-xs text-slate-600 dark:text-gray-400 truncate">
|
||||
{currentRun ? `Started: ${new Date(currentRun.started_at).toLocaleTimeString()}` : (totalPending > 0 ? `${totalPending} items in pipeline` : 'All stages clear')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DebugSiteSelector />
|
||||
</div>
|
||||
|
||||
{/* Compact Schedule & Controls Panel */}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,13 +7,15 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ComponentCard from '../../components/common/ComponentCard';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Button from '../../components/ui/button/Button';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { fetchAPI, fetchSiteSectors } from '../../services/api';
|
||||
import SiteSetupChecklist from '../../components/sites/SiteSetupChecklist';
|
||||
import { integrationApi } from '../../services/integration.api';
|
||||
import SiteConfigWidget from '../../components/dashboard/SiteConfigWidget';
|
||||
import OperationsCostsWidget from '../../components/dashboard/OperationsCostsWidget';
|
||||
import CreditAvailabilityWidget from '../../components/dashboard/CreditAvailabilityWidget';
|
||||
import { useBillingStore } from '../../store/billingStore';
|
||||
import {
|
||||
FileIcon,
|
||||
PlugInIcon,
|
||||
@@ -21,7 +23,6 @@ import {
|
||||
BoltIcon,
|
||||
PageIcon,
|
||||
ArrowRightIcon,
|
||||
ArrowUpIcon
|
||||
} from '../../icons';
|
||||
|
||||
interface Site {
|
||||
@@ -42,28 +43,46 @@ interface Site {
|
||||
interface SiteSetupState {
|
||||
hasIndustry: boolean;
|
||||
hasSectors: boolean;
|
||||
sectorsCount: number;
|
||||
hasWordPressIntegration: boolean;
|
||||
hasKeywords: boolean;
|
||||
keywordsCount: number;
|
||||
hasAuthorProfiles: boolean;
|
||||
authorProfilesCount: number;
|
||||
}
|
||||
|
||||
interface OperationStat {
|
||||
type: 'clustering' | 'ideas' | 'content' | 'images';
|
||||
count: number;
|
||||
creditsUsed: number;
|
||||
avgCreditsPerOp: number;
|
||||
}
|
||||
|
||||
export default function SiteDashboard() {
|
||||
const { id: siteId } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const { balance, loadBalance } = useBillingStore();
|
||||
const [site, setSite] = useState<Site | null>(null);
|
||||
const [setupState, setSetupState] = useState<SiteSetupState>({
|
||||
hasIndustry: false,
|
||||
hasSectors: false,
|
||||
sectorsCount: 0,
|
||||
hasWordPressIntegration: false,
|
||||
hasKeywords: false,
|
||||
keywordsCount: 0,
|
||||
hasAuthorProfiles: false,
|
||||
authorProfilesCount: 0,
|
||||
});
|
||||
const [operations, setOperations] = useState<OperationStat[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (siteId) {
|
||||
loadSiteData();
|
||||
loadBalance();
|
||||
}
|
||||
}, [siteId]);
|
||||
}, [siteId, loadBalance]);
|
||||
|
||||
const loadSiteData = async () => {
|
||||
try {
|
||||
@@ -79,9 +98,11 @@ export default function SiteDashboard() {
|
||||
|
||||
// Load sectors
|
||||
let hasSectors = false;
|
||||
let sectorsCount = 0;
|
||||
try {
|
||||
const sectors = await fetchSiteSectors(Number(siteId));
|
||||
hasSectors = sectors && sectors.length > 0;
|
||||
sectorsCount = sectors?.length || 0;
|
||||
} catch (err) {
|
||||
console.log('Could not load sectors');
|
||||
}
|
||||
@@ -97,20 +118,47 @@ export default function SiteDashboard() {
|
||||
|
||||
// Check keywords - try to load keywords for this site
|
||||
let hasKeywords = false;
|
||||
let keywordsCount = 0;
|
||||
try {
|
||||
const { fetchKeywords } = await import('../../services/api');
|
||||
const keywordsData = await fetchKeywords({ site_id: Number(siteId), page_size: 1 });
|
||||
hasKeywords = keywordsData?.results?.length > 0 || keywordsData?.count > 0;
|
||||
keywordsCount = keywordsData?.count || 0;
|
||||
} catch (err) {
|
||||
// No keywords is fine
|
||||
}
|
||||
|
||||
// Check author profiles
|
||||
let hasAuthorProfiles = false;
|
||||
let authorProfilesCount = 0;
|
||||
try {
|
||||
const authorsData = await fetchAPI(`/v1/thinker/author-profiles/?site_id=${siteId}&page_size=1`);
|
||||
hasAuthorProfiles = authorsData?.count > 0;
|
||||
authorProfilesCount = authorsData?.count || 0;
|
||||
} catch (err) {
|
||||
// No profiles is fine
|
||||
}
|
||||
|
||||
setSetupState({
|
||||
hasIndustry,
|
||||
hasSectors,
|
||||
sectorsCount,
|
||||
hasWordPressIntegration,
|
||||
hasKeywords,
|
||||
keywordsCount,
|
||||
hasAuthorProfiles,
|
||||
authorProfilesCount,
|
||||
});
|
||||
|
||||
// Load operation stats (mock data for now - would come from backend)
|
||||
// In real implementation, fetch from /api/v1/dashboard/site/{siteId}/operations/
|
||||
const mockOperations: OperationStat[] = [
|
||||
{ type: 'clustering', count: 8, creditsUsed: 80, avgCreditsPerOp: 10 },
|
||||
{ type: 'ideas', count: 12, creditsUsed: 24, avgCreditsPerOp: 2 },
|
||||
{ type: 'content', count: 28, creditsUsed: 1400, avgCreditsPerOp: 50 },
|
||||
{ type: 'images', count: 45, creditsUsed: 225, avgCreditsPerOp: 5 },
|
||||
];
|
||||
setOperations(mockOperations);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to load site data: ${error.message}`);
|
||||
@@ -185,6 +233,28 @@ export default function SiteDashboard() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Site Insights - 3 Column Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
<SiteConfigWidget
|
||||
setupState={{
|
||||
hasIndustry: setupState.hasIndustry,
|
||||
sectorsCount: setupState.sectorsCount,
|
||||
hasWordPressIntegration: setupState.hasWordPressIntegration,
|
||||
keywordsCount: setupState.keywordsCount,
|
||||
authorProfilesCount: setupState.authorProfilesCount
|
||||
}}
|
||||
siteId={Number(siteId)}
|
||||
/>
|
||||
|
||||
<OperationsCostsWidget operations={operations} siteId={Number(siteId)} />
|
||||
|
||||
<CreditAvailabilityWidget
|
||||
availableCredits={balance?.credits_remaining ?? 0}
|
||||
totalCredits={balance?.plan_credits_per_month ?? 0}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<ComponentCard title="Quick Actions" desc="Common site management tasks">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
|
||||
@@ -17,6 +17,8 @@ import SelectDropdown from '../../components/form/SelectDropdown';
|
||||
import Label from '../../components/form/Label';
|
||||
import Checkbox from '../../components/form/input/Checkbox';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import { BoxCubeIcon } from '../../icons';
|
||||
|
||||
type TabType = 'content' | 'publishing' | 'images';
|
||||
|
||||
@@ -325,19 +327,16 @@ export default function ContentSettingsPage() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<PageMeta title="Content Settings" description="Configure your content generation settings" />
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="mb-6">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 mb-1">
|
||||
Content Settings / {tabTitles[activeTab]}
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">{tabTitles[activeTab]}</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
{activeTab === 'content' && 'Customize how your articles are written'}
|
||||
{activeTab === 'publishing' && 'Configure automatic publishing settings'}
|
||||
{activeTab === 'images' && 'Set up AI image generation preferences'}
|
||||
</p>
|
||||
</div>
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user