Add new fields to TasksSerializer and enhance auto_generate_content_task with detailed step tracking

- Updated TasksSerializer to include 'primary_keyword', 'secondary_keywords', 'tags', and 'categories'.
- Enhanced auto_generate_content_task to track progress with detailed steps, including initialization, preparation, AI call, parsing, and saving.
- Updated progress modal to reflect new phases and improved animation for smoother user experience.
- Adjusted routing and configuration for content and drafts pages in the frontend.
This commit is contained in:
IGNY8 VPS (Salman)
2025-11-10 13:17:48 +00:00
parent b1d86cd4b8
commit 8bb4c5d016
14 changed files with 247 additions and 47 deletions

View File

@@ -1,5 +1,5 @@
import { Suspense, lazy } from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router";
import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router";
import AppLayout from "./layout/AppLayout";
import { ScrollToTop } from "./components/common/ScrollToTop";
import ProtectedRoute from "./components/auth/ProtectedRoute";
@@ -24,7 +24,6 @@ const KeywordOpportunities = lazy(() => import("./pages/Planner/KeywordOpportuni
// Writer Module - Lazy loaded
const WriterDashboard = lazy(() => import("./pages/Writer/Dashboard"));
const Tasks = lazy(() => import("./pages/Writer/Tasks"));
const Content = lazy(() => import("./pages/Writer/Content"));
const Drafts = lazy(() => import("./pages/Writer/Drafts"));
const Images = lazy(() => import("./pages/Writer/Images"));
const Published = lazy(() => import("./pages/Writer/Published"));
@@ -160,15 +159,11 @@ export default function App() {
</Suspense>
} />
<Route path="/writer/content" element={
<Suspense fallback={null}>
<Content />
</Suspense>
} />
<Route path="/writer/drafts" element={
<Suspense fallback={null}>
<Drafts />
</Suspense>
} />
<Route path="/writer/drafts" element={<Navigate to="/writer/content" replace />} />
<Route path="/writer/images" element={
<Suspense fallback={null}>
<Images />

View File

@@ -146,7 +146,7 @@ export default function ProgressModal({
className="max-w-lg"
showCloseButton={status === 'completed' || status === 'error'}
>
<div className="p-6">
<div className="p-6 min-h-[200px]">
{/* Header */}
<div className="flex items-start gap-4 mb-6">
<div className="flex-shrink-0 mt-1">{getStatusIcon()}</div>

View File

@@ -99,7 +99,7 @@ export const bulkActionModalConfigs: Record<string, BulkActionModalConfig> = {
],
},
},
'/writer/drafts': {
'/writer/content': {
export: {
title: 'Export Selected Drafts',
message: (count: number) => `You are about to export ${count} selected draft${count !== 1 ? 's' : ''}. The export will be downloaded as a CSV file.`,

View File

@@ -40,7 +40,7 @@ export const deleteModalConfigs: Record<string, DeleteModalConfig> = {
itemNameSingular: 'task',
itemNamePlural: 'tasks',
},
'/writer/drafts': {
'/writer/content': {
title: 'Delete Drafts',
singleItemMessage: 'You are about to delete this draft. This action cannot be undone.',
multipleItemsMessage: (count: number) => `You are deleting ${count} drafts. This action cannot be undone.`,

View File

@@ -37,7 +37,7 @@ export const pageNotifications: Record<string, PageNotificationConfig> = {
message: 'Track and manage your content writing tasks and deadlines.',
showLink: false,
},
'/writer/drafts': {
'/writer/content': {
variant: 'info',
title: 'Drafts',
message: 'Review and edit your content drafts before publishing.',

View File

@@ -249,7 +249,7 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
// Removed generate_content from bulk actions - only available as row action
],
},
'/writer/drafts': {
'/writer/content': {
rowActions: [
{
key: 'edit',

View File

@@ -97,11 +97,18 @@ export const createTasksPageConfig = (
columns: [
{
...titleColumn,
key: 'title',
label: 'Title',
sortable: true,
sortField: 'title',
toggleable: true, // Enable toggle for this column
toggleContentKey: 'content', // Use content field for toggle (fallback to description if content not available)
toggleContentLabel: 'Generated Content', // Label for expanded content
render: (_value: string, row: Task) => (
<span className="text-gray-800 dark:text-white font-medium">
{row.meta_title || row.title || '-'}
</span>
),
},
// Sector column - only show when viewing all sectors
...(showSectorColumn ? [{
@@ -166,6 +173,85 @@ export const createTasksPageConfig = (
);
},
},
{
key: 'keywords',
label: 'Keywords',
sortable: false,
width: '250px',
render: (_value: any, row: Task) => {
const keywords: React.ReactNode[] = [];
// Primary keyword as info badge
if (row.primary_keyword) {
keywords.push(
<Badge key="primary" color="info" size="sm" variant="light" className="mr-1 mb-1">
{row.primary_keyword}
</Badge>
);
}
// Secondary keywords as light badges
if (row.secondary_keywords && Array.isArray(row.secondary_keywords) && row.secondary_keywords.length > 0) {
row.secondary_keywords.forEach((keyword, index) => {
if (keyword) {
keywords.push(
<Badge key={`secondary-${index}`} color="light" size="sm" variant="light" className="mr-1 mb-1">
{keyword}
</Badge>
);
}
});
}
return keywords.length > 0 ? (
<div className="flex flex-wrap gap-1">
{keywords}
</div>
) : (
<span className="text-gray-400">-</span>
);
},
},
{
key: 'tags',
label: 'Tags',
sortable: false,
width: '200px',
render: (_value: any, row: Task) => {
if (row.tags && Array.isArray(row.tags) && row.tags.length > 0) {
return (
<div className="flex flex-wrap gap-1">
{row.tags.map((tag, index) => (
<Badge key={index} color="light" size="sm" variant="light">
{tag}
</Badge>
))}
</div>
);
}
return <span className="text-gray-400">-</span>;
},
},
{
key: 'categories',
label: 'Categories',
sortable: false,
width: '200px',
render: (_value: any, row: Task) => {
if (row.categories && Array.isArray(row.categories) && row.categories.length > 0) {
return (
<div className="flex flex-wrap gap-1">
{row.categories.map((category, index) => (
<Badge key={index} color="light" size="sm" variant="light">
{category}
</Badge>
))}
</div>
);
}
return <span className="text-gray-400">-</span>;
},
},
{
...wordCountColumn,
sortable: true,

View File

@@ -36,7 +36,7 @@ export const routes: RouteConfig[] = [
children: [
{ path: '/writer', label: 'Dashboard', breadcrumb: 'Writer Dashboard' },
{ path: '/writer/tasks', label: 'Tasks', breadcrumb: 'Tasks' },
{ path: '/writer/drafts', label: 'Drafts', breadcrumb: 'Drafts' },
{ path: '/writer/content', label: 'Content', breadcrumb: 'Content' },
{ path: '/writer/published', label: 'Published', breadcrumb: 'Published' },
],
},

View File

@@ -250,21 +250,37 @@ export function useProgressModal(): UseProgressModalReturn {
// Fallback to phase or message if no step found
if (!currentStep) {
const currentPhase = (meta.phase || '').toLowerCase();
const currentPhase = (meta.phase || '').toUpperCase();
const currentMessage = (meta.message || '').toLowerCase();
if (currentPhase.includes('initializ') || currentMessage.includes('initializ') || currentMessage.includes('getting started')) {
// Check exact phase match first (backend now sends uppercase phase names)
if (currentPhase === 'INIT' || currentPhase.includes('INIT')) {
currentStep = 'INIT';
} else if (currentPhase.includes('prepar') || currentPhase.includes('prep') || currentMessage.includes('prepar') || currentMessage.includes('loading')) {
} else if (currentPhase === 'PREP' || currentPhase.includes('PREP')) {
currentStep = 'PREP';
} else if (currentPhase.includes('analyzing') || currentPhase.includes('ai_call') || currentMessage.includes('analyzing') || currentMessage.includes('finding related')) {
} else if (currentPhase === 'AI_CALL' || currentPhase.includes('AI_CALL') || currentPhase.includes('CALL')) {
currentStep = 'AI_CALL';
} else if (currentPhase.includes('pars') || currentMessage.includes('pars') || currentMessage.includes('organizing')) {
} else if (currentPhase === 'PARSE' || currentPhase.includes('PARSE')) {
currentStep = 'PARSE';
} else if (currentPhase.includes('sav') || currentPhase.includes('creat') || currentMessage.includes('sav') || currentMessage.includes('creat') || (currentMessage.includes('cluster') && !currentMessage.includes('content'))) {
} else if (currentPhase === 'SAVE' || currentPhase.includes('SAVE') || currentPhase.includes('CREAT')) {
currentStep = 'SAVE';
} else if (currentPhase.includes('done') || currentPhase.includes('complet') || currentMessage.includes('done') || currentMessage.includes('complet')) {
} else if (currentPhase === 'DONE' || currentPhase.includes('DONE') || currentPhase.includes('COMPLETE')) {
currentStep = 'DONE';
} else {
// Fallback to message-based detection
if (currentMessage.includes('initializ') || currentMessage.includes('getting started')) {
currentStep = 'INIT';
} else if (currentMessage.includes('prepar') || currentMessage.includes('loading')) {
currentStep = 'PREP';
} else if (currentMessage.includes('generating') || currentMessage.includes('analyzing') || currentMessage.includes('finding related')) {
currentStep = 'AI_CALL';
} else if (currentMessage.includes('pars') || currentMessage.includes('organizing') || currentMessage.includes('processing content')) {
currentStep = 'PARSE';
} else if (currentMessage.includes('sav') || currentMessage.includes('creat') || (currentMessage.includes('cluster') && !currentMessage.includes('content'))) {
currentStep = 'SAVE';
} else if (currentMessage.includes('done') || currentMessage.includes('complet')) {
currentStep = 'DONE';
}
}
}
@@ -273,7 +289,9 @@ export function useProgressModal(): UseProgressModalReturn {
// Include title in message for better function type detection
const messageWithContext = `${title} ${originalMessage}`;
const stepInfo = getStepInfo(currentStep || '', messageWithContext, allSteps);
const targetPercentage = stepInfo.percentage;
// Use backend percentage if available, otherwise use step-based percentage
const backendPercentage = meta.percentage !== undefined ? meta.percentage : null;
const targetPercentage = backendPercentage !== null ? backendPercentage : stepInfo.percentage;
const friendlyMessage = stepInfo.friendlyMessage;
// Check if we're transitioning to a new step
@@ -286,13 +304,17 @@ export function useProgressModal(): UseProgressModalReturn {
stepTransitionTimeoutRef.current = null;
}
// Smooth progress animation: increment by 1% every 300ms until reaching target
// Smooth progress animation: increment gradually until reaching target
// Use smaller increments and faster updates for smoother animation
if (targetPercentage > currentDisplayedPercentage) {
// Start smooth animation
let animatedPercentage = currentDisplayedPercentage;
const animateProgress = () => {
if (animatedPercentage < targetPercentage) {
animatedPercentage = Math.min(animatedPercentage + 1, targetPercentage);
// Calculate increment: smaller increments for smoother animation
const diff = targetPercentage - animatedPercentage;
const increment = diff > 10 ? 2 : 1; // Larger jumps for big differences
animatedPercentage = Math.min(animatedPercentage + increment, targetPercentage);
displayedPercentageRef.current = animatedPercentage;
setProgress({
percentage: animatedPercentage,
@@ -308,25 +330,26 @@ export function useProgressModal(): UseProgressModalReturn {
});
if (animatedPercentage < targetPercentage) {
stepTransitionTimeoutRef.current = setTimeout(animateProgress, 300);
// Faster updates: 200ms for smoother feel
stepTransitionTimeoutRef.current = setTimeout(animateProgress, 200);
} else {
stepTransitionTimeoutRef.current = null;
}
}
};
// If it's a new step, add 500ms delay before starting animation
// If it's a new step, add small delay before starting animation
if (isNewStep && currentStepRef.current !== null) {
stepTransitionTimeoutRef.current = setTimeout(() => {
currentStepRef.current = currentStep;
animateProgress();
}, 500);
}, 300);
} else {
// Same step or first step - start animation immediately
currentStepRef.current = currentStep;
animateProgress();
}
} else {
} else if (targetPercentage !== currentDisplayedPercentage) {
// Percentage decreased or same - update immediately (shouldn't happen normally)
currentStepRef.current = currentStep;
displayedPercentageRef.current = targetPercentage;
@@ -342,6 +365,20 @@ export function useProgressModal(): UseProgressModalReturn {
phase: meta.phase,
},
});
} else {
// Same percentage - just update message and details if needed
currentStepRef.current = currentStep;
setProgress(prev => ({
...prev,
message: friendlyMessage,
details: {
current: meta.current || 0,
total: meta.total || 0,
completed: meta.completed || 0,
currentItem: meta.current_item,
phase: meta.phase,
},
}));
}
// Update step logs if available

View File

@@ -108,7 +108,6 @@ const AppSidebar: React.FC = () => {
{ name: "Dashboard", path: "/writer" },
{ name: "Tasks", path: "/writer/tasks" },
{ name: "Content", path: "/writer/content" },
{ name: "Drafts", path: "/writer/drafts" },
{ name: "Images", path: "/writer/images" },
{ name: "Published", path: "/writer/published" },
],

View File

@@ -1071,6 +1071,10 @@ export interface Task {
word_count: number;
meta_title?: string | null;
meta_description?: string | null;
primary_keyword?: string | null;
secondary_keywords?: string[] | null;
tags?: string[] | null;
categories?: string[] | null;
assigned_post_id?: number | null;
post_url?: string | null;
created_at: string;