page adn app header mods
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import TablePageTemplate from '../../templates/TablePageTemplate';
|
||||
import {
|
||||
fetchClusters,
|
||||
@@ -28,7 +29,6 @@ import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||
import { getDifficultyLabelFromNumber, getDifficultyRange } from '../../utils/difficulty';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
|
||||
import { WorkflowInsight } from '../../components/common/WorkflowInsights';
|
||||
|
||||
export default function Clusters() {
|
||||
const toast = useToast();
|
||||
@@ -76,61 +76,6 @@ export default function Clusters() {
|
||||
const progressModal = useProgressModal();
|
||||
const hasReloadedRef = useRef(false);
|
||||
|
||||
// Calculate workflow insights
|
||||
const workflowInsights: WorkflowInsight[] = useMemo(() => {
|
||||
const insights: WorkflowInsight[] = [];
|
||||
const clustersWithIdeas = clusters.filter(c => (c.ideas_count || 0) > 0).length;
|
||||
const totalIdeas = clusters.reduce((sum, c) => sum + (c.ideas_count || 0), 0);
|
||||
const emptyClusters = clusters.filter(c => (c.keywords_count || 0) === 0).length;
|
||||
const thinClusters = clusters.filter(c => (c.keywords_count || 0) > 0 && (c.keywords_count || 0) < 3).length;
|
||||
const readyForGeneration = clustersWithIdeas;
|
||||
const generationRate = totalCount > 0 ? Math.round((readyForGeneration / totalCount) * 100) : 0;
|
||||
|
||||
if (totalCount === 0) {
|
||||
insights.push({
|
||||
type: 'info',
|
||||
message: 'Create clusters to organize keywords into topical groups for better content planning',
|
||||
});
|
||||
return insights;
|
||||
}
|
||||
|
||||
// Content generation readiness
|
||||
if (generationRate < 30) {
|
||||
insights.push({
|
||||
type: 'warning',
|
||||
message: `Only ${generationRate}% of clusters have content ideas - Generate ideas to unlock content pipeline`,
|
||||
});
|
||||
} else if (generationRate >= 70) {
|
||||
insights.push({
|
||||
type: 'success',
|
||||
message: `${generationRate}% of clusters have ideas (${totalIdeas} total) - Strong content pipeline ready`,
|
||||
});
|
||||
}
|
||||
|
||||
// Empty or thin clusters
|
||||
if (emptyClusters > 0) {
|
||||
insights.push({
|
||||
type: 'warning',
|
||||
message: `${emptyClusters} clusters have no keywords - Map keywords or delete unused clusters`,
|
||||
});
|
||||
} else if (thinClusters > 2) {
|
||||
insights.push({
|
||||
type: 'info',
|
||||
message: `${thinClusters} clusters have fewer than 3 keywords - Consider adding more related keywords for better coverage`,
|
||||
});
|
||||
}
|
||||
|
||||
// Actionable next step
|
||||
if (totalIdeas === 0) {
|
||||
insights.push({
|
||||
type: 'action',
|
||||
message: 'Select clusters and use Auto-Generate Ideas to create content briefs',
|
||||
});
|
||||
}
|
||||
|
||||
return insights;
|
||||
}, [clusters, totalCount]);
|
||||
|
||||
// Load clusters - wrapped in useCallback to prevent infinite loops
|
||||
const loadClusters = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -445,10 +390,20 @@ export default function Clusters() {
|
||||
<>
|
||||
<PageHeader
|
||||
title="Clusters"
|
||||
description="Keyword groups organized by topic. Generate content ideas from clusters to build topical authority."
|
||||
description="Group keywords into topic clusters"
|
||||
badge={{ icon: <GroupIcon />, color: 'purple' }}
|
||||
breadcrumb="Planner / Clusters"
|
||||
workflowInsights={workflowInsights}
|
||||
breadcrumb="Planner"
|
||||
actions={
|
||||
<Link
|
||||
to="/planner/ideas"
|
||||
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-brand-500 hover:bg-brand-600 rounded-lg transition-colors"
|
||||
>
|
||||
Generate Ideas
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import TablePageTemplate from '../../templates/TablePageTemplate';
|
||||
import {
|
||||
fetchContentIdeas,
|
||||
@@ -24,12 +25,12 @@ import ProgressModal from '../../components/common/ProgressModal';
|
||||
import { useProgressModal } from '../../hooks/useProgressModal';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { BoltIcon, PlusIcon, DownloadIcon, ListIcon, GroupIcon } from '../../icons';
|
||||
import { LightBulbIcon } from '@heroicons/react/24/outline';
|
||||
import { createIdeasPageConfig } from '../../config/pages/ideas.config';
|
||||
import { useSectorStore } from '../../store/sectorStore';
|
||||
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
|
||||
import { WorkflowInsight } from '../../components/common/WorkflowInsights';
|
||||
|
||||
export default function Ideas() {
|
||||
const toast = useToast();
|
||||
@@ -77,57 +78,6 @@ export default function Ideas() {
|
||||
// Progress modal for AI functions
|
||||
const progressModal = useProgressModal();
|
||||
|
||||
// Calculate workflow insights
|
||||
const workflowInsights: WorkflowInsight[] = useMemo(() => {
|
||||
const insights: WorkflowInsight[] = [];
|
||||
const newCount = ideas.filter(i => i.status === 'new').length;
|
||||
const queuedCount = ideas.filter(i => i.status === 'queued').length;
|
||||
const completedCount = ideas.filter(i => i.status === 'completed').length;
|
||||
const queueActivationRate = totalCount > 0 ? Math.round((queuedCount / totalCount) * 100) : 0;
|
||||
const completionRate = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;
|
||||
|
||||
if (totalCount === 0) {
|
||||
insights.push({
|
||||
type: 'info',
|
||||
message: 'Generate ideas from your keyword clusters to build your content pipeline',
|
||||
});
|
||||
return insights;
|
||||
}
|
||||
|
||||
// Queue activation insights
|
||||
if (newCount > 0 && queuedCount === 0) {
|
||||
insights.push({
|
||||
type: 'warning',
|
||||
message: `${newCount} new ideas waiting - Queue them to activate the content pipeline`,
|
||||
});
|
||||
} else if (queueActivationRate > 0 && queueActivationRate < 40) {
|
||||
insights.push({
|
||||
type: 'info',
|
||||
message: `${queueActivationRate}% of ideas queued (${queuedCount} ideas) - Queue more ideas to maintain steady content flow`,
|
||||
});
|
||||
} else if (queuedCount > 0) {
|
||||
insights.push({
|
||||
type: 'success',
|
||||
message: `${queuedCount} ideas in queue - Content pipeline is active and ready for task generation`,
|
||||
});
|
||||
}
|
||||
|
||||
// Completion velocity
|
||||
if (completionRate >= 50) {
|
||||
insights.push({
|
||||
type: 'success',
|
||||
message: `Strong completion rate (${completionRate}%) - ${completedCount} ideas converted to content`,
|
||||
});
|
||||
} else if (completionRate > 0) {
|
||||
insights.push({
|
||||
type: 'info',
|
||||
message: `${completedCount} ideas completed (${completionRate}%) - Continue queuing ideas to grow content library`,
|
||||
});
|
||||
}
|
||||
|
||||
return insights;
|
||||
}, [ideas, totalCount]);
|
||||
|
||||
// Load clusters for filter dropdown
|
||||
useEffect(() => {
|
||||
const loadClusters = async () => {
|
||||
@@ -351,10 +301,20 @@ export default function Ideas() {
|
||||
<>
|
||||
<PageHeader
|
||||
title="Ideas"
|
||||
description="AI-generated content ideas with titles, outlines, and target keywords. Queue ideas to start content generation."
|
||||
badge={{ icon: <BoltIcon />, color: 'orange' }}
|
||||
breadcrumb="Planner / Ideas"
|
||||
workflowInsights={workflowInsights}
|
||||
description="Content ideas generated from keywords"
|
||||
badge={{ icon: <LightBulbIcon />, color: 'yellow' }}
|
||||
breadcrumb="Planner"
|
||||
actions={
|
||||
<Link
|
||||
to="/writer/queue"
|
||||
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-brand-500 hover:bg-brand-600 rounded-lg transition-colors"
|
||||
>
|
||||
Start Writing
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import TablePageTemplate from '../../templates/TablePageTemplate';
|
||||
import {
|
||||
fetchKeywords,
|
||||
@@ -346,6 +347,18 @@ export default function Keywords() {
|
||||
}
|
||||
}, [toast, activeSector, loadKeywords, progressModal, keywords]);
|
||||
|
||||
// Quick auto-cluster unclustered keywords (for Next Step button)
|
||||
const handleAutoCluster = useCallback(async () => {
|
||||
const unclusteredIds = keywords.filter(k => !k.cluster_id).map(k => k.id);
|
||||
if (unclusteredIds.length === 0) {
|
||||
toast.info('All keywords are already clustered');
|
||||
return;
|
||||
}
|
||||
// Limit to 50 keywords
|
||||
const idsToCluster = unclusteredIds.slice(0, 50);
|
||||
await handleBulkAction('auto_cluster', idsToCluster.map(String));
|
||||
}, [keywords, handleBulkAction, toast]);
|
||||
|
||||
// Reset reload flag when modal closes or opens
|
||||
useEffect(() => {
|
||||
if (!progressModal.isOpen) {
|
||||
@@ -481,63 +494,32 @@ export default function Keywords() {
|
||||
}, [pageConfig?.headerMetrics, keywords, totalCount, clusters]);
|
||||
|
||||
// Calculate workflow insights based on UX doc principles
|
||||
const workflowInsights = useMemo(() => {
|
||||
const insights = [];
|
||||
const workflowStats = useMemo(() => {
|
||||
const clusteredCount = keywords.filter(k => k.cluster_id).length;
|
||||
const unclusteredCount = totalCount - clusteredCount;
|
||||
const pipelineReadiness = totalCount > 0 ? Math.round((clusteredCount / totalCount) * 100) : 0;
|
||||
|
||||
return {
|
||||
total: totalCount,
|
||||
clustered: clusteredCount,
|
||||
unclustered: unclusteredCount,
|
||||
readiness: pipelineReadiness,
|
||||
};
|
||||
}, [keywords, totalCount]);
|
||||
|
||||
// Determine next step action
|
||||
const nextStep = useMemo(() => {
|
||||
if (totalCount === 0) {
|
||||
insights.push({
|
||||
type: 'info' as const,
|
||||
message: 'Import keywords to begin building your content strategy and unlock SEO opportunities',
|
||||
});
|
||||
return insights;
|
||||
return { label: 'Import Keywords', path: '/add-keywords', disabled: false };
|
||||
}
|
||||
|
||||
// Pipeline Readiness Score insight
|
||||
if (pipelineReadiness < 30) {
|
||||
insights.push({
|
||||
type: 'warning' as const,
|
||||
message: `Pipeline readiness at ${pipelineReadiness}% - Most keywords need clustering before content ideation can begin`,
|
||||
});
|
||||
} else if (pipelineReadiness < 60) {
|
||||
insights.push({
|
||||
type: 'info' as const,
|
||||
message: `Pipeline readiness at ${pipelineReadiness}% - Clustering progress is moderate, continue organizing keywords`,
|
||||
});
|
||||
} else if (pipelineReadiness >= 85) {
|
||||
insights.push({
|
||||
type: 'success' as const,
|
||||
message: `Excellent pipeline readiness (${pipelineReadiness}%) - Ready for content ideation phase`,
|
||||
});
|
||||
if (workflowStats.unclustered >= 5) {
|
||||
return { label: 'Auto-Cluster', action: 'cluster', disabled: false };
|
||||
}
|
||||
|
||||
// Clustering Potential (minimum batch size check)
|
||||
if (unclusteredCount >= 5) {
|
||||
insights.push({
|
||||
type: 'action' as const,
|
||||
message: `${unclusteredCount} keywords available for auto-clustering (minimum batch size met)`,
|
||||
});
|
||||
} else if (unclusteredCount > 0 && unclusteredCount < 5) {
|
||||
insights.push({
|
||||
type: 'info' as const,
|
||||
message: `${unclusteredCount} unclustered keywords - Need ${5 - unclusteredCount} more to run auto-cluster`,
|
||||
});
|
||||
if (workflowStats.clustered > 0) {
|
||||
return { label: 'Generate Ideas', path: '/planner/ideas', disabled: false };
|
||||
}
|
||||
|
||||
// Coverage Gaps - thin clusters that need more research
|
||||
const thinClusters = clusters.filter(c => (c.keywords_count || 0) === 1);
|
||||
if (thinClusters.length > 3) {
|
||||
const thinVolume = thinClusters.reduce((sum, c) => sum + (c.volume || 0), 0);
|
||||
insights.push({
|
||||
type: 'warning' as const,
|
||||
message: `${thinClusters.length} clusters have only 1 keyword each (${thinVolume.toLocaleString()} monthly volume) - Consider expanding research`,
|
||||
});
|
||||
}
|
||||
|
||||
return insights;
|
||||
}, [keywords, totalCount, clusters]);
|
||||
return { label: 'Add More Keywords', path: '/add-keywords', disabled: false };
|
||||
}, [totalCount, workflowStats]);
|
||||
|
||||
// Handle create/edit
|
||||
const handleSave = async () => {
|
||||
@@ -612,10 +594,37 @@ export default function Keywords() {
|
||||
<>
|
||||
<PageHeader
|
||||
title="Keywords"
|
||||
description="Your target search terms organized for content creation. Import, cluster, and transform into content ideas."
|
||||
description="Your target search terms organized for content creation"
|
||||
badge={{ icon: <ListIcon />, color: 'green' }}
|
||||
breadcrumb="Planner / Keywords"
|
||||
workflowInsights={workflowInsights}
|
||||
breadcrumb="Planner"
|
||||
actions={
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 hidden md:block">
|
||||
{workflowStats.clustered}/{workflowStats.total} clustered
|
||||
</span>
|
||||
{nextStep.path ? (
|
||||
<Link
|
||||
to={nextStep.path}
|
||||
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-brand-500 hover:bg-brand-600 rounded-lg transition-colors"
|
||||
>
|
||||
{nextStep.label}
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
) : nextStep.action === 'cluster' ? (
|
||||
<button
|
||||
onClick={handleAutoCluster}
|
||||
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-brand-500 hover:bg-brand-600 rounded-lg transition-colors"
|
||||
>
|
||||
{nextStep.label}
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
|
||||
@@ -150,6 +150,7 @@ export default function SiteDashboard() {
|
||||
<PageHeader
|
||||
title={site.name}
|
||||
badge={{ icon: <GridIcon />, color: 'blue' }}
|
||||
breadcrumb="Sites / Dashboard"
|
||||
/>
|
||||
|
||||
{/* Site Info */}
|
||||
@@ -267,6 +268,8 @@ export default function SiteDashboard() {
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
No recent activity
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
bulkDeleteContent,
|
||||
} from '../../services/api';
|
||||
import { optimizerApi } from '../../api/optimizer.api';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { FileIcon, TaskIcon, CheckCircleIcon } from '../../icons';
|
||||
import { createContentPageConfig } from '../../config/pages/content.config';
|
||||
@@ -24,7 +24,7 @@ import ProgressModal from '../../components/common/ProgressModal';
|
||||
import { useProgressModal } from '../../hooks/useProgressModal';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
|
||||
import { WorkflowInsight } from '../../components/common/WorkflowInsights';
|
||||
import { PencilSquareIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export default function Content() {
|
||||
const toast = useToast();
|
||||
@@ -55,59 +55,6 @@ export default function Content() {
|
||||
const progressModal = useProgressModal();
|
||||
const hasReloadedRef = useRef(false);
|
||||
|
||||
// Calculate workflow insights
|
||||
const workflowInsights: WorkflowInsight[] = useMemo(() => {
|
||||
const insights: WorkflowInsight[] = [];
|
||||
const draftCount = content.filter(c => c.status === 'draft').length;
|
||||
const reviewCount = content.filter(c => c.status === 'review').length;
|
||||
const publishedCount = content.filter(c => c.status === 'published').length;
|
||||
const publishingRate = totalCount > 0 ? Math.round((publishedCount / totalCount) * 100) : 0;
|
||||
|
||||
if (totalCount === 0) {
|
||||
insights.push({
|
||||
type: 'info',
|
||||
message: 'No content yet - Generate content from tasks to build your content library',
|
||||
});
|
||||
return insights;
|
||||
}
|
||||
|
||||
// Draft vs Review status
|
||||
if (draftCount > reviewCount * 3 && draftCount >= 5) {
|
||||
insights.push({
|
||||
type: 'warning',
|
||||
message: `${draftCount} drafts waiting for review - Move content to review stage for quality assurance`,
|
||||
});
|
||||
} else if (draftCount > 0) {
|
||||
insights.push({
|
||||
type: 'info',
|
||||
message: `${draftCount} drafts in progress - Review and refine before moving to publish stage`,
|
||||
});
|
||||
}
|
||||
|
||||
// Review queue status
|
||||
if (reviewCount > 0) {
|
||||
insights.push({
|
||||
type: 'action',
|
||||
message: `${reviewCount} pieces awaiting final review - Approve and publish when ready`,
|
||||
});
|
||||
}
|
||||
|
||||
// Publishing readiness
|
||||
if (publishingRate >= 60 && publishedCount >= 10) {
|
||||
insights.push({
|
||||
type: 'success',
|
||||
message: `Strong publishing rate (${publishingRate}%) - ${publishedCount} articles ready for WordPress sync`,
|
||||
});
|
||||
} else if (publishedCount > 0) {
|
||||
insights.push({
|
||||
type: 'success',
|
||||
message: `${publishedCount} articles published (${publishingRate}%) - Continue moving content through the pipeline`,
|
||||
});
|
||||
}
|
||||
|
||||
return insights;
|
||||
}, [content, totalCount]);
|
||||
|
||||
// Load content - wrapped in useCallback
|
||||
const loadContent = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -281,10 +228,20 @@ export default function Content() {
|
||||
<>
|
||||
<PageHeader
|
||||
title="Drafts"
|
||||
description="AI-generated content ready for review. Add images, edit, and publish when ready."
|
||||
badge={{ icon: <FileIcon />, color: 'purple' }}
|
||||
breadcrumb="Writer / Drafts"
|
||||
workflowInsights={workflowInsights}
|
||||
description="Manage content drafts"
|
||||
badge={{ icon: <PencilSquareIcon />, color: 'orange' }}
|
||||
breadcrumb="Writer"
|
||||
actions={
|
||||
<Link
|
||||
to="/writer/images"
|
||||
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-brand-500 hover:bg-brand-600 rounded-lg transition-colors"
|
||||
>
|
||||
Generate Images
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import TablePageTemplate from '../../templates/TablePageTemplate';
|
||||
import {
|
||||
fetchContentImages,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
} from '../../services/api';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { FileIcon, DownloadIcon } from '../../icons';
|
||||
import { PhotoIcon } from '@heroicons/react/24/outline';
|
||||
import { createImagesPageConfig } from '../../config/pages/images.config';
|
||||
import ImageQueueModal, { ImageQueueItem } from '../../components/common/ImageQueueModal';
|
||||
import SingleRecordStatusUpdateModal from '../../components/common/SingleRecordStatusUpdateModal';
|
||||
@@ -450,8 +452,20 @@ export default function Images() {
|
||||
<>
|
||||
<PageHeader
|
||||
title="Images"
|
||||
badge={{ icon: <FileIcon />, color: 'orange' }}
|
||||
breadcrumb="Writer / Images"
|
||||
description="Generate and manage content images"
|
||||
badge={{ icon: <PhotoIcon />, color: 'pink' }}
|
||||
breadcrumb="Writer"
|
||||
actions={
|
||||
<Link
|
||||
to="/writer/review"
|
||||
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-brand-500 hover:bg-brand-600 rounded-lg transition-colors"
|
||||
>
|
||||
Review Content
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import TablePageTemplate from '../../templates/TablePageTemplate';
|
||||
import {
|
||||
fetchContent,
|
||||
@@ -15,9 +16,9 @@ import {
|
||||
deleteContent,
|
||||
bulkDeleteContent,
|
||||
} from '../../services/api';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { FileIcon, CheckCircleIcon } from '../../icons';
|
||||
import { RocketLaunchIcon } from '@heroicons/react/24/outline';
|
||||
import { createPublishedPageConfig } from '../../config/pages/published.config';
|
||||
import { useSectorStore } from '../../store/sectorStore';
|
||||
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||
@@ -308,8 +309,20 @@ export default function Published() {
|
||||
<>
|
||||
<PageHeader
|
||||
title="Published"
|
||||
badge={{ icon: <CheckCircleIcon />, color: 'green' }}
|
||||
breadcrumb="Writer / Published"
|
||||
description="Published content and WordPress sync status"
|
||||
badge={{ icon: <RocketLaunchIcon />, color: 'green' }}
|
||||
breadcrumb="Writer"
|
||||
actions={
|
||||
<Link
|
||||
to="/planner/keywords"
|
||||
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-brand-500 hover:bg-brand-600 rounded-lg transition-colors"
|
||||
>
|
||||
Create More Content
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import TablePageTemplate from '../../templates/TablePageTemplate';
|
||||
import {
|
||||
fetchContent,
|
||||
@@ -12,9 +13,9 @@ import {
|
||||
ContentFilters,
|
||||
fetchAPI,
|
||||
} from '../../services/api';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { CheckCircleIcon } from '../../icons';
|
||||
import { ClipboardDocumentCheckIcon } from '@heroicons/react/24/outline';
|
||||
import { createReviewPageConfig } from '../../config/pages/review.config';
|
||||
import { useSectorStore } from '../../store/sectorStore';
|
||||
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||
@@ -346,9 +347,21 @@ export default function Review() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Review Queue"
|
||||
badge={{ icon: <CheckCircleIcon />, color: 'blue' }}
|
||||
breadcrumb="Writer / Review"
|
||||
title="Review"
|
||||
description="Review and approve content before publishing"
|
||||
badge={{ icon: <ClipboardDocumentCheckIcon />, color: 'emerald' }}
|
||||
breadcrumb="Writer"
|
||||
actions={
|
||||
<Link
|
||||
to="/writer/published"
|
||||
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-brand-500 hover:bg-brand-600 rounded-lg transition-colors"
|
||||
>
|
||||
View Published
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* Consistent with Keywords page layout, structure and design
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import TablePageTemplate from '../../templates/TablePageTemplate';
|
||||
import {
|
||||
fetchTasks,
|
||||
@@ -30,7 +31,7 @@ import { useSectorStore } from '../../store/sectorStore';
|
||||
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
|
||||
import { WorkflowInsight } from '../../components/common/WorkflowInsights';
|
||||
import { DocumentTextIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export default function Tasks() {
|
||||
const toast = useToast();
|
||||
@@ -79,63 +80,6 @@ export default function Tasks() {
|
||||
// Progress modal for AI functions
|
||||
const progressModal = useProgressModal();
|
||||
|
||||
// Calculate workflow insights
|
||||
const workflowInsights: WorkflowInsight[] = useMemo(() => {
|
||||
const insights: WorkflowInsight[] = [];
|
||||
const queuedCount = tasks.filter(t => t.status === 'queued').length;
|
||||
const processingCount = tasks.filter(t => t.status === 'in_progress').length;
|
||||
const completedCount = tasks.filter(t => t.status === 'completed').length;
|
||||
const failedCount = tasks.filter(t => t.status === 'failed').length;
|
||||
const completionRate = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;
|
||||
|
||||
if (totalCount === 0) {
|
||||
insights.push({
|
||||
type: 'info',
|
||||
message: 'No tasks yet - Queue ideas from Planner to start generating content automatically',
|
||||
});
|
||||
return insights;
|
||||
}
|
||||
|
||||
// Queue status
|
||||
if (queuedCount > 10) {
|
||||
insights.push({
|
||||
type: 'warning',
|
||||
message: `Large queue detected (${queuedCount} tasks) - Content generation may take time, consider prioritizing`,
|
||||
});
|
||||
} else if (queuedCount > 0) {
|
||||
insights.push({
|
||||
type: 'info',
|
||||
message: `${queuedCount} tasks in queue - Content generation pipeline is active`,
|
||||
});
|
||||
}
|
||||
|
||||
// Processing status
|
||||
if (processingCount > 0) {
|
||||
insights.push({
|
||||
type: 'action',
|
||||
message: `${processingCount} tasks actively generating content - Check back soon for completed drafts`,
|
||||
});
|
||||
}
|
||||
|
||||
// Failed tasks
|
||||
if (failedCount > 0) {
|
||||
insights.push({
|
||||
type: 'warning',
|
||||
message: `${failedCount} tasks failed - Review errors and retry or adjust task parameters`,
|
||||
});
|
||||
}
|
||||
|
||||
// Completion success
|
||||
if (completionRate >= 70 && completedCount >= 5) {
|
||||
insights.push({
|
||||
type: 'success',
|
||||
message: `High completion rate (${completionRate}%) - ${completedCount} pieces of content ready for review`,
|
||||
});
|
||||
}
|
||||
|
||||
return insights;
|
||||
}, [tasks, totalCount]);
|
||||
|
||||
// AI Function Logs state
|
||||
|
||||
|
||||
@@ -424,11 +368,21 @@ export default function Tasks() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Content Queue"
|
||||
description="Manage content tasks waiting for AI generation. Queue ideas here to create articles automatically."
|
||||
badge={{ icon: <TaskIcon />, color: 'indigo' }}
|
||||
breadcrumb="Writer / Queue"
|
||||
workflowInsights={workflowInsights}
|
||||
title="Queue"
|
||||
description="Content writing queue"
|
||||
badge={{ icon: <DocumentTextIcon />, color: 'blue' }}
|
||||
breadcrumb="Writer"
|
||||
actions={
|
||||
<Link
|
||||
to="/writer/content"
|
||||
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-brand-500 hover:bg-brand-600 rounded-lg transition-colors"
|
||||
>
|
||||
View Drafts
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
|
||||
@@ -384,7 +384,7 @@ export default function PlansAndBillingPage() {
|
||||
<div className="mb-4 p-4 rounded-lg border border-amber-200 bg-amber-50 text-amber-800 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-200">
|
||||
No active plan. Choose a plan below to activate your account.
|
||||
</div>
|
||||
)}}
|
||||
)}
|
||||
{hasPendingManualPayment && (
|
||||
<div className="mb-4 p-4 rounded-lg border border-[var(--color-info-200)] bg-[var(--color-info-50)] text-[var(--color-info-800)] dark:border-[var(--color-info-800)] dark:bg-[var(--color-info-900)]/20 dark:text-[var(--color-info-100)]">
|
||||
We received your manual payment. It’s pending admin approval; activation will complete once approved.
|
||||
|
||||
Reference in New Issue
Block a user