diff --git a/frontend/src/components/common/PageHeader.tsx b/frontend/src/components/common/PageHeader.tsx index 625043a4..133e9863 100644 --- a/frontend/src/components/common/PageHeader.tsx +++ b/frontend/src/components/common/PageHeader.tsx @@ -1,20 +1,18 @@ /** * Standardized Page Header Component - * Used across all Planner and Writer module pages - * Includes: Page title, last updated, site/sector info, and selectors + * Simplified version - Site/sector selector moved to AppHeader + * Just shows: breadcrumb (inline), page title with badge, description */ import React, { ReactNode, useEffect, useRef } from 'react'; import { useSiteStore } from '../../store/siteStore'; import { useSectorStore } from '../../store/sectorStore'; -import SiteAndSectorSelector from './SiteAndSectorSelector'; import { trackLoading } from './LoadingStateMonitor'; import { useErrorHandler } from '../../hooks/useErrorHandler'; -import { WorkflowInsights, WorkflowInsight } from './WorkflowInsights'; interface PageHeaderProps { title: string; - description?: string; // Optional page description shown below title - breadcrumb?: string; // Optional breadcrumb text (e.g., "Thinker / Prompts") + description?: string; + breadcrumb?: string; lastUpdated?: Date; showRefresh?: boolean; onRefresh?: () => void; @@ -23,9 +21,11 @@ interface PageHeaderProps { icon: ReactNode; color: 'blue' | 'green' | 'purple' | 'orange' | 'red' | 'indigo'; }; - hideSiteSector?: boolean; // Hide site/sector selector and info for global pages - navigation?: ReactNode; // Module navigation tabs - workflowInsights?: WorkflowInsight[]; // Actionable insights for current page + hideSiteSector?: boolean; + navigation?: ReactNode; // Kept for backwards compat but not rendered + workflowInsights?: any[]; // Kept for backwards compat but not rendered + /** Right-side actions slot */ + actions?: ReactNode; } export default function PageHeader({ @@ -38,47 +38,35 @@ export default function PageHeader({ className = "", badge, hideSiteSector = false, - navigation, - workflowInsights, + actions, }: PageHeaderProps) { const { activeSite } = useSiteStore(); - const { activeSector, loadSectorsForSite } = useSectorStore(); + const { loadSectorsForSite } = useSectorStore(); const { addError } = useErrorHandler('PageHeader'); const lastSiteId = useRef(null); const isLoadingSector = useRef(false); - // Load sectors when active site changes - only for pages that need site/sector context + // Load sectors when active site changes useEffect(() => { - // Skip sector loading for pages that hide site/sector selector (account/billing pages) if (hideSiteSector) return; - const currentSiteId = activeSite?.id ?? null; - // Only load if: - // 1. We have a site ID - // 2. The site is active (inactive sites can't have accessible sectors) - // 3. It's different from the last one we loaded - // 4. We're not already loading if (currentSiteId && activeSite?.is_active && currentSiteId !== lastSiteId.current && !isLoadingSector.current) { lastSiteId.current = currentSiteId; isLoadingSector.current = true; trackLoading('sector-loading', true); - // Add timeout to prevent infinite loading const timeoutId = setTimeout(() => { if (isLoadingSector.current) { - console.error('PageHeader: Sector loading timeout after 35 seconds'); trackLoading('sector-loading', false); isLoadingSector.current = false; - addError(new Error('Sector loading timeout - check network connection'), 'PageHeader.loadSectorsForSite'); + addError(new Error('Sector loading timeout'), 'PageHeader.loadSectorsForSite'); } }, 35000); loadSectorsForSite(currentSiteId) .catch((error) => { - // Don't log 403/404 errors as they're expected for inactive sites if (error.status !== 403 && error.status !== 404) { - console.error('PageHeader: Error loading sectors:', error); addError(error, 'PageHeader.loadSectorsForSite'); } }) @@ -88,7 +76,6 @@ export default function PageHeader({ isLoadingSector.current = false; }); } else if (currentSiteId && !activeSite?.is_active) { - // Site is inactive - clear sectors and reset lastSiteId lastSiteId.current = null; const { useSectorStore } = require('../../store/sectorStore'); useSectorStore.getState().clearActiveSector(); @@ -105,98 +92,47 @@ export default function PageHeader({ }; return ( -
- {/* Breadcrumb */} - {breadcrumb && ( -
- {breadcrumb} -
- )} - - {/* Main header row - single row with 3 sections */} -
- {/* Left side: Title, badge, and site/sector info */} -
-
- {badge && ( -
- {badge.icon && typeof badge.icon === 'object' && 'type' in badge.icon - ? React.cloneElement(badge.icon as React.ReactElement, { className: 'text-white size-5' }) - : badge.icon} -
- )} -

{title}

-
- {description && ( -

{description}

- )} - {!hideSiteSector && ( -
- {lastUpdated && ( - <> -

- Last updated: {lastUpdated.toLocaleTimeString()} -

- - )} - {activeSite && ( - <> - {lastUpdated && } -

- Site: {activeSite.name} -

- - )} - {activeSector && ( - <> - -

- Sector: {activeSector.name} -

- - )} - {!activeSector && activeSite && ( - <> - -

- Sector: All Sectors -

- - )} -
- )} - {hideSiteSector && lastUpdated && ( -
-

- Last updated: {lastUpdated.toLocaleTimeString()} -

-
- )} -
- - {/* Middle: Workflow insights - takes available space */} - {workflowInsights && workflowInsights.length > 0 && ( -
- +
+ {/* Left: Breadcrumb + Badge + Title */} +
+ {breadcrumb && ( + <> + {breadcrumb} + / + + )} + {badge && ( +
+ {badge.icon && typeof badge.icon === 'object' && 'type' in badge.icon + ? React.cloneElement(badge.icon as React.ReactElement, { className: 'text-white size-4' }) + : badge.icon}
)} - - {/* Right side: Navigation bar stacked above site/sector selector */} -
- {navigation &&
{navigation}
} -
- {!hideSiteSector && } - {showRefresh && onRefresh && ( - - )} -
+
+

{title}

+ {description && ( +

{description}

+ )}
+ + {/* Right: Actions */} +
+ {lastUpdated && ( + + Updated {lastUpdated.toLocaleTimeString()} + + )} + {showRefresh && onRefresh && ( + + )} + {actions} +
); } diff --git a/frontend/src/components/common/SearchModal.tsx b/frontend/src/components/common/SearchModal.tsx new file mode 100644 index 00000000..75490558 --- /dev/null +++ b/frontend/src/components/common/SearchModal.tsx @@ -0,0 +1,136 @@ +/** + * Search Modal - Global search modal triggered by icon or Cmd+K + */ +import { useState, useEffect, useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Modal } from '../ui/modal'; + +interface SearchModalProps { + isOpen: boolean; + onClose: () => void; +} + +interface SearchResult { + title: string; + path: string; + type: 'page' | 'action'; + icon?: string; +} + +const SEARCH_ITEMS: SearchResult[] = [ + // Workflow + { title: 'Keywords', path: '/planner/keywords', type: 'page' }, + { title: 'Clusters', path: '/planner/clusters', type: 'page' }, + { title: 'Ideas', path: '/planner/ideas', type: 'page' }, + { title: 'Queue', path: '/writer/tasks', type: 'page' }, + { title: 'Drafts', path: '/writer/content', type: 'page' }, + { title: 'Images', path: '/writer/images', type: 'page' }, + { title: 'Review', path: '/writer/review', type: 'page' }, + { title: 'Published', path: '/writer/published', type: 'page' }, + // Setup + { title: 'Sites', path: '/sites', type: 'page' }, + { title: 'Add Keywords', path: '/add-keywords', type: 'page' }, + { title: 'Content Settings', path: '/account/content-settings', type: 'page' }, + { title: 'Prompts', path: '/thinker/prompts', type: 'page' }, + { title: 'Author Profiles', path: '/thinker/author-profiles', type: 'page' }, + // Account + { title: 'Account Settings', path: '/account/settings', type: 'page' }, + { title: 'Plans & Billing', path: '/account/plans', type: 'page' }, + { title: 'Usage Analytics', path: '/account/usage', type: 'page' }, + // Help + { title: 'Help & Support', path: '/help', type: 'page' }, +]; + +export default function SearchModal({ isOpen, onClose }: SearchModalProps) { + const [query, setQuery] = useState(''); + const [selectedIndex, setSelectedIndex] = useState(0); + const inputRef = useRef(null); + const navigate = useNavigate(); + + const filteredResults = query.length > 0 + ? SEARCH_ITEMS.filter(item => + item.title.toLowerCase().includes(query.toLowerCase()) + ) + : SEARCH_ITEMS.slice(0, 8); + + useEffect(() => { + if (isOpen) { + setQuery(''); + setSelectedIndex(0); + setTimeout(() => inputRef.current?.focus(), 100); + } + }, [isOpen]); + + useEffect(() => { + setSelectedIndex(0); + }, [query]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + setSelectedIndex(i => Math.min(i + 1, filteredResults.length - 1)); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setSelectedIndex(i => Math.max(i - 1, 0)); + } else if (e.key === 'Enter' && filteredResults[selectedIndex]) { + navigate(filteredResults[selectedIndex].path); + onClose(); + } + }; + + const handleSelect = (result: SearchResult) => { + navigate(result.path); + onClose(); + }; + + return ( + +
+
+ + + + + + setQuery(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Search pages..." + className="w-full pl-12 pr-4 py-4 text-lg border-b border-gray-200 dark:border-gray-700 bg-transparent focus:outline-none dark:text-white" + /> + + ESC to close + +
+ +
+ {filteredResults.length === 0 ? ( +
+ No results found for "{query}" +
+ ) : ( + filteredResults.map((result, index) => ( + + )) + )} +
+
+
+ ); +} diff --git a/frontend/src/context/PageContext.tsx b/frontend/src/context/PageContext.tsx new file mode 100644 index 00000000..e98aaddf --- /dev/null +++ b/frontend/src/context/PageContext.tsx @@ -0,0 +1,48 @@ +/** + * Page Context - Shares current page info with header + * Allows pages to set title, breadcrumb, badge for display in AppHeader + */ +import React, { createContext, useContext, useState, ReactNode } from 'react'; + +interface PageInfo { + title: string; + breadcrumb?: string; + badge?: { + icon: ReactNode; + color: 'blue' | 'green' | 'purple' | 'orange' | 'red' | 'indigo'; + }; +} + +interface PageContextType { + pageInfo: PageInfo | null; + setPageInfo: (info: PageInfo | null) => void; +} + +const PageContext = createContext(undefined); + +export function PageProvider({ children }: { children: ReactNode }) { + const [pageInfo, setPageInfo] = useState(null); + + return ( + + {children} + + ); +} + +export function usePageContext() { + const context = useContext(PageContext); + if (context === undefined) { + throw new Error('usePageContext must be used within a PageProvider'); + } + return context; +} + +export function usePage(info: PageInfo) { + const { setPageInfo } = usePageContext(); + + React.useEffect(() => { + setPageInfo(info); + return () => setPageInfo(null); + }, [info.title, info.breadcrumb]); +} diff --git a/frontend/src/layout/AppHeader.tsx b/frontend/src/layout/AppHeader.tsx index e2e01b21..a4e21aa2 100644 --- a/frontend/src/layout/AppHeader.tsx +++ b/frontend/src/layout/AppHeader.tsx @@ -1,14 +1,16 @@ -import { useEffect, useRef, useState } from "react"; - +import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { useSidebar } from "../context/SidebarContext"; import { ThemeToggleButton } from "../components/common/ThemeToggleButton"; import NotificationDropdown from "../components/header/NotificationDropdown"; import UserDropdown from "../components/header/UserDropdown"; import { HeaderMetrics } from "../components/header/HeaderMetrics"; +import SearchModal from "../components/common/SearchModal"; +import SiteAndSectorSelector from "../components/common/SiteAndSectorSelector"; const AppHeader: React.FC = () => { const [isApplicationMenuOpen, setApplicationMenuOpen] = useState(false); + const [isSearchOpen, setIsSearchOpen] = useState(false); const { isMobileOpen, toggleSidebar, toggleMobileSidebar } = useSidebar(); @@ -24,153 +26,100 @@ const AppHeader: React.FC = () => { setApplicationMenuOpen(!isApplicationMenuOpen); }; - const inputRef = useRef(null); - + // Keyboard shortcut for search useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if ((event.metaKey || event.ctrlKey) && event.key === "k") { event.preventDefault(); - inputRef.current?.focus(); + setIsSearchOpen(true); } }; document.addEventListener("keydown", handleKeyDown); - - return () => { - document.removeEventListener("keydown", handleKeyDown); - }; + return () => document.removeEventListener("keydown", handleKeyDown); }, []); return ( -
-
-
- - - - Logo - Logo - - - + {isMobileOpen ? ( + + + + ) : ( + + + + )} + -
-
-
- - - - - - + {/* Mobile Logo */} + + Logo + Logo + - -
-
+ {/* Mobile Menu Toggle */} + + + {/* Site and Sector Selector - Desktop */} +
+ +
+
+ + {/* Right side actions */} +
+
+ {/* Header Metrics */} + + + {/* Search Icon */} + + + {/* Dark Mode Toggler */} + + + {/* Notifications */} + +
+ + {/* User Menu */} +
-
-
- {/* */} - - {/* */} - - {/* */} - - {/* */} -
- {/* */} - -
-
-
+ + + {/* Search Modal */} + setIsSearchOpen(false)} /> + ); }; diff --git a/frontend/src/pages/Planner/Clusters.tsx b/frontend/src/pages/Planner/Clusters.tsx index 4b4ca62b..2c77e140 100644 --- a/frontend/src/pages/Planner/Clusters.tsx +++ b/frontend/src/pages/Planner/Clusters.tsx @@ -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() { <> , color: 'purple' }} - breadcrumb="Planner / Clusters" - workflowInsights={workflowInsights} + breadcrumb="Planner" + actions={ + + Generate Ideas + + + + + } /> { - 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() { <> , color: 'orange' }} - breadcrumb="Planner / Ideas" - workflowInsights={workflowInsights} + description="Content ideas generated from keywords" + badge={{ icon: , color: 'yellow' }} + breadcrumb="Planner" + actions={ + + Start Writing + + + + + } /> { + 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() { <> , color: 'green' }} - breadcrumb="Planner / Keywords" - workflowInsights={workflowInsights} + breadcrumb="Planner" + actions={ +
+ + {workflowStats.clustered}/{workflowStats.total} clustered + + {nextStep.path ? ( + + {nextStep.label} + + + + + ) : nextStep.action === 'cluster' ? ( + + ) : null} +
+ } /> , color: 'blue' }} + breadcrumb="Sites / Dashboard" /> {/* Site Info */} @@ -267,6 +268,8 @@ export default function SiteDashboard() {

No recent activity

+ +
); } diff --git a/frontend/src/pages/Writer/Content.tsx b/frontend/src/pages/Writer/Content.tsx index f2750d0b..c5ec0f2c 100644 --- a/frontend/src/pages/Writer/Content.tsx +++ b/frontend/src/pages/Writer/Content.tsx @@ -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() { <> , color: 'purple' }} - breadcrumb="Writer / Drafts" - workflowInsights={workflowInsights} + description="Manage content drafts" + badge={{ icon: , color: 'orange' }} + breadcrumb="Writer" + actions={ + + Generate Images + + + + + } /> , color: 'orange' }} - breadcrumb="Writer / Images" + description="Generate and manage content images" + badge={{ icon: , color: 'pink' }} + breadcrumb="Writer" + actions={ + + Review Content + + + + + } /> , color: 'green' }} - breadcrumb="Writer / Published" + description="Published content and WordPress sync status" + badge={{ icon: , color: 'green' }} + breadcrumb="Writer" + actions={ + + Create More Content + + + + + } /> , color: 'blue' }} - breadcrumb="Writer / Review" + title="Review" + description="Review and approve content before publishing" + badge={{ icon: , color: 'emerald' }} + breadcrumb="Writer" + actions={ + + View Published + + + + + } /> { - 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 ( <> , color: 'indigo' }} - breadcrumb="Writer / Queue" - workflowInsights={workflowInsights} + title="Queue" + description="Content writing queue" + badge={{ icon: , color: 'blue' }} + breadcrumb="Writer" + actions={ + + View Drafts + + + + + } /> No active plan. Choose a plan below to activate your account.
- )}} + )} {hasPendingManualPayment && (
We received your manual payment. It’s pending admin approval; activation will complete once approved.