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

@@ -30,6 +30,10 @@ class TasksSerializer(serializers.ModelSerializer):
'word_count', 'word_count',
'meta_title', 'meta_title',
'meta_description', 'meta_description',
'primary_keyword',
'secondary_keywords',
'tags',
'categories',
'assigned_post_id', 'assigned_post_id',
'post_url', 'post_url',
'created_at', 'created_at',

View File

@@ -3,6 +3,7 @@ Celery tasks for Writer module - AI content generation
""" """
import logging import logging
import re import re
import time
from typing import List from typing import List
from django.db import transaction from django.db import transaction
from igny8_core.modules.writer.models import Tasks, Images, Content from igny8_core.modules.writer.models import Tasks, Images, Content
@@ -35,7 +36,29 @@ def auto_generate_content_task(self, task_ids: List[int], account_id: int = None
account_id: Account ID for account isolation account_id: Account ID for account isolation
""" """
try: try:
# Step tracking for progress modal
step_counter = 0
request_steps = []
response_steps = []
def add_step(step_name, status='success', message='', step_type='request'):
nonlocal step_counter
step_counter += 1
step = {
'stepNumber': step_counter,
'stepName': step_name,
'status': status,
'message': message,
'timestamp': time.time()
}
if step_type == 'request':
request_steps.append(step)
else:
response_steps.append(step)
return step
# Initialize progress # Initialize progress
add_step('INIT', 'success', 'Initializing content generation...', 'request')
self.update_state( self.update_state(
state='PROGRESS', state='PROGRESS',
meta={ meta={
@@ -43,7 +66,9 @@ def auto_generate_content_task(self, task_ids: List[int], account_id: int = None
'total': len(task_ids), 'total': len(task_ids),
'percentage': 0, 'percentage': 0,
'message': 'Initializing content generation...', 'message': 'Initializing content generation...',
'phase': 'initializing' 'phase': 'INIT',
'request_steps': request_steps,
'response_steps': response_steps
} }
) )
@@ -190,15 +215,18 @@ def auto_generate_content_task(self, task_ids: List[int], account_id: int = None
total_tasks = len(tasks) total_tasks = len(tasks)
# Update progress: Preparing tasks (0-5%) # Update progress: Preparing tasks
add_step('PREP', 'success', f'Preparing {total_tasks} tasks for content generation...', 'request')
self.update_state( self.update_state(
state='PROGRESS', state='PROGRESS',
meta={ meta={
'current': 0, 'current': 0,
'total': total_tasks, 'total': total_tasks,
'percentage': 2, 'percentage': 10,
'message': f'Preparing {total_tasks} tasks for content generation...', 'message': f'Preparing {total_tasks} tasks for content generation...',
'phase': 'preparing' 'phase': 'PREP',
'request_steps': request_steps,
'response_steps': response_steps
} }
) )
@@ -286,17 +314,21 @@ def auto_generate_content_task(self, task_ids: List[int], account_id: int = None
logger.warning(f" - Task idea_id: {task.idea_id}") logger.warning(f" - Task idea_id: {task.idea_id}")
# Don't skip - idea is nullable # Don't skip - idea is nullable
# Update progress: Processing task (5-90%) # Update progress: Processing task
progress_pct = 5 + int((idx / total_tasks) * 85) # Calculate base percentage: 10% (PREP) + progress through tasks (10-50%)
base_pct = 10
task_progress_pct = base_pct + int((idx / total_tasks) * 40) # 10-50% for task prep
self.update_state( self.update_state(
state='PROGRESS', state='PROGRESS',
meta={ meta={
'current': idx + 1, 'current': idx + 1,
'total': total_tasks, 'total': total_tasks,
'percentage': progress_pct, 'percentage': task_progress_pct,
'message': f"Generating content for '{task.title}' ({idx + 1} of {total_tasks})...", 'message': f"Preparing content generation for '{task.title}' ({idx + 1} of {total_tasks})...",
'phase': 'generating', 'phase': 'PREP',
'current_item': task.title 'current_item': task.title,
'request_steps': request_steps,
'response_steps': response_steps
} }
) )
@@ -496,15 +528,19 @@ def auto_generate_content_task(self, task_ids: List[int], account_id: int = None
logger.info("=" * 80) logger.info("=" * 80)
# Update progress: Generating with AI # Update progress: Generating with AI
add_step('AI_CALL', 'success', f"Generating article content for '{task.title}'...", 'request')
ai_call_pct = 50 + int((idx / total_tasks) * 20) # 50-70% for AI call
self.update_state( self.update_state(
state='PROGRESS', state='PROGRESS',
meta={ meta={
'current': idx + 1, 'current': idx + 1,
'total': total_tasks, 'total': total_tasks,
'percentage': progress_pct, 'percentage': ai_call_pct,
'message': f"Generating article content for '{task.title}'...", 'message': f"Generating article content for '{task.title}'...",
'phase': 'generating', 'phase': 'AI_CALL',
'current_item': task.title 'current_item': task.title,
'request_steps': request_steps,
'response_steps': response_steps
} }
) )
@@ -579,6 +615,23 @@ def auto_generate_content_task(self, task_ids: List[int], account_id: int = None
logger.info(f" * Cost: ${result.get('cost', 'N/A')}") logger.info(f" * Cost: ${result.get('cost', 'N/A')}")
logger.info(f" * Raw content preview (first 200 chars): {content[:200]}...") logger.info(f" * Raw content preview (first 200 chars): {content[:200]}...")
# Update progress: Parsing content
add_step('PARSE', 'success', f"Processing content for '{task.title}'...", 'response')
parse_pct = 70 + int((idx / total_tasks) * 10) # 70-80% for parsing
self.update_state(
state='PROGRESS',
meta={
'current': idx + 1,
'total': total_tasks,
'percentage': parse_pct,
'message': f"Processing content for '{task.title}'...",
'phase': 'PARSE',
'current_item': task.title,
'request_steps': request_steps,
'response_steps': response_steps
}
)
# Normalize content from different AI response formats # Normalize content from different AI response formats
logger.info(f" * Normalizing content (length: {len(content)} chars)...") logger.info(f" * Normalizing content (length: {len(content)} chars)...")
try: try:
@@ -616,15 +669,19 @@ def auto_generate_content_task(self, task_ids: List[int], account_id: int = None
logger.info(f" * ✓ Word count calculated: {word_count} words (from normalized HTML)") logger.info(f" * ✓ Word count calculated: {word_count} words (from normalized HTML)")
# Update progress: Saving content # Update progress: Saving content
add_step('SAVE', 'success', f"Saving content for '{task.title}' ({word_count} words)...", 'request')
save_pct = 85 + int((idx / total_tasks) * 10) # 85-95% for saving
self.update_state( self.update_state(
state='PROGRESS', state='PROGRESS',
meta={ meta={
'current': idx + 1, 'current': idx + 1,
'total': total_tasks, 'total': total_tasks,
'percentage': progress_pct, 'percentage': save_pct,
'message': f"Saving content for '{task.title}' ({word_count} words)...", 'message': f"Saving content for '{task.title}' ({word_count} words)...",
'phase': 'saving', 'phase': 'SAVE',
'current_item': task.title 'current_item': task.title,
'request_steps': request_steps,
'response_steps': response_steps
} }
) )
@@ -668,6 +725,9 @@ def auto_generate_content_task(self, task_ids: List[int], account_id: int = None
task.save() task.save()
logger.info(f" * ✓ Task saved successfully to database") logger.info(f" * ✓ Task saved successfully to database")
# Mark save step as complete
add_step('SAVE', 'success', f"Content saved for '{task.title}'", 'response')
tasks_updated += 1 tasks_updated += 1
logger.info(f" * ✓ Task {task.id} content generation completed successfully") logger.info(f" * ✓ Task {task.id} content generation completed successfully")
@@ -687,8 +747,9 @@ def auto_generate_content_task(self, task_ids: List[int], account_id: int = None
logger.info(f"✓ TASK {task.id} PROCESSING COMPLETE") logger.info(f"✓ TASK {task.id} PROCESSING COMPLETE")
logger.info("=" * 80) logger.info("=" * 80)
# Final progress update # Final progress update - mark as DONE
final_message = f"Content generation complete: {tasks_updated} articles generated" final_message = f"Content generation complete: {tasks_updated} articles generated"
add_step('DONE', 'success', final_message, 'response')
logger.info("=" * 80) logger.info("=" * 80)
logger.info(f"TASK COMPLETION SUMMARY") logger.info(f"TASK COMPLETION SUMMARY")
logger.info(f" - Total tasks processed: {total_tasks}") logger.info(f" - Total tasks processed: {total_tasks}")
@@ -696,6 +757,21 @@ def auto_generate_content_task(self, task_ids: List[int], account_id: int = None
logger.info(f" - Tasks failed/skipped: {total_tasks - tasks_updated}") logger.info(f" - Tasks failed/skipped: {total_tasks - tasks_updated}")
logger.info("=" * 80) logger.info("=" * 80)
# Update final state before returning
self.update_state(
state='SUCCESS',
meta={
'current': total_tasks,
'total': total_tasks,
'percentage': 100,
'message': final_message,
'phase': 'DONE',
'request_steps': request_steps,
'response_steps': response_steps,
'tasks_updated': tasks_updated
}
)
return { return {
'success': True, 'success': True,
'tasks_updated': tasks_updated, 'tasks_updated': tasks_updated,

View File

@@ -163,8 +163,7 @@ frontend/src/
- `/planner/ideas` - Ideas page - `/planner/ideas` - Ideas page
- `/writer` - Writer Dashboard - `/writer` - Writer Dashboard
- `/writer/tasks` - Tasks page - `/writer/tasks` - Tasks page
- `/writer/content` - Content page - `/writer/content` - Drafts page
- `/writer/drafts` - Drafts page
- `/writer/images` - Images page - `/writer/images` - Images page
- `/writer/published` - Published page - `/writer/published` - Published page
- `/thinker` - Thinker Dashboard - `/thinker` - Thinker Dashboard

View File

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

View File

@@ -146,7 +146,7 @@ export default function ProgressModal({
className="max-w-lg" className="max-w-lg"
showCloseButton={status === 'completed' || status === 'error'} showCloseButton={status === 'completed' || status === 'error'}
> >
<div className="p-6"> <div className="p-6 min-h-[200px]">
{/* Header */} {/* Header */}
<div className="flex items-start gap-4 mb-6"> <div className="flex items-start gap-4 mb-6">
<div className="flex-shrink-0 mt-1">{getStatusIcon()}</div> <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: { export: {
title: 'Export Selected Drafts', 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.`, 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', itemNameSingular: 'task',
itemNamePlural: 'tasks', itemNamePlural: 'tasks',
}, },
'/writer/drafts': { '/writer/content': {
title: 'Delete Drafts', title: 'Delete Drafts',
singleItemMessage: 'You are about to delete this draft. This action cannot be undone.', 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.`, 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.', message: 'Track and manage your content writing tasks and deadlines.',
showLink: false, showLink: false,
}, },
'/writer/drafts': { '/writer/content': {
variant: 'info', variant: 'info',
title: 'Drafts', title: 'Drafts',
message: 'Review and edit your content drafts before publishing.', 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 // Removed generate_content from bulk actions - only available as row action
], ],
}, },
'/writer/drafts': { '/writer/content': {
rowActions: [ rowActions: [
{ {
key: 'edit', key: 'edit',

View File

@@ -97,11 +97,18 @@ export const createTasksPageConfig = (
columns: [ columns: [
{ {
...titleColumn, ...titleColumn,
key: 'title',
label: 'Title',
sortable: true, sortable: true,
sortField: 'title', sortField: 'title',
toggleable: true, // Enable toggle for this column toggleable: true, // Enable toggle for this column
toggleContentKey: 'content', // Use content field for toggle (fallback to description if content not available) toggleContentKey: 'content', // Use content field for toggle (fallback to description if content not available)
toggleContentLabel: 'Generated Content', // Label for expanded content 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 // Sector column - only show when viewing all sectors
...(showSectorColumn ? [{ ...(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, ...wordCountColumn,
sortable: true, sortable: true,

View File

@@ -36,7 +36,7 @@ export const routes: RouteConfig[] = [
children: [ children: [
{ path: '/writer', label: 'Dashboard', breadcrumb: 'Writer Dashboard' }, { path: '/writer', label: 'Dashboard', breadcrumb: 'Writer Dashboard' },
{ path: '/writer/tasks', label: 'Tasks', breadcrumb: 'Tasks' }, { 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' }, { 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 // Fallback to phase or message if no step found
if (!currentStep) { if (!currentStep) {
const currentPhase = (meta.phase || '').toLowerCase(); const currentPhase = (meta.phase || '').toUpperCase();
const currentMessage = (meta.message || '').toLowerCase(); 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'; 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'; 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'; currentStep = 'AI_CALL';
} else if (currentPhase.includes('pars') || currentMessage.includes('pars') || currentMessage.includes('organizing')) { } else if (currentPhase === 'PARSE' || currentPhase.includes('PARSE')) {
currentStep = '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'; 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'; 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 // Include title in message for better function type detection
const messageWithContext = `${title} ${originalMessage}`; const messageWithContext = `${title} ${originalMessage}`;
const stepInfo = getStepInfo(currentStep || '', messageWithContext, allSteps); 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; const friendlyMessage = stepInfo.friendlyMessage;
// Check if we're transitioning to a new step // Check if we're transitioning to a new step
@@ -286,13 +304,17 @@ export function useProgressModal(): UseProgressModalReturn {
stepTransitionTimeoutRef.current = null; 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) { if (targetPercentage > currentDisplayedPercentage) {
// Start smooth animation // Start smooth animation
let animatedPercentage = currentDisplayedPercentage; let animatedPercentage = currentDisplayedPercentage;
const animateProgress = () => { const animateProgress = () => {
if (animatedPercentage < targetPercentage) { 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; displayedPercentageRef.current = animatedPercentage;
setProgress({ setProgress({
percentage: animatedPercentage, percentage: animatedPercentage,
@@ -308,25 +330,26 @@ export function useProgressModal(): UseProgressModalReturn {
}); });
if (animatedPercentage < targetPercentage) { if (animatedPercentage < targetPercentage) {
stepTransitionTimeoutRef.current = setTimeout(animateProgress, 300); // Faster updates: 200ms for smoother feel
stepTransitionTimeoutRef.current = setTimeout(animateProgress, 200);
} else { } else {
stepTransitionTimeoutRef.current = null; 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) { if (isNewStep && currentStepRef.current !== null) {
stepTransitionTimeoutRef.current = setTimeout(() => { stepTransitionTimeoutRef.current = setTimeout(() => {
currentStepRef.current = currentStep; currentStepRef.current = currentStep;
animateProgress(); animateProgress();
}, 500); }, 300);
} else { } else {
// Same step or first step - start animation immediately // Same step or first step - start animation immediately
currentStepRef.current = currentStep; currentStepRef.current = currentStep;
animateProgress(); animateProgress();
} }
} else { } else if (targetPercentage !== currentDisplayedPercentage) {
// Percentage decreased or same - update immediately (shouldn't happen normally) // Percentage decreased or same - update immediately (shouldn't happen normally)
currentStepRef.current = currentStep; currentStepRef.current = currentStep;
displayedPercentageRef.current = targetPercentage; displayedPercentageRef.current = targetPercentage;
@@ -342,6 +365,20 @@ export function useProgressModal(): UseProgressModalReturn {
phase: meta.phase, 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 // Update step logs if available

View File

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

View File

@@ -1071,6 +1071,10 @@ export interface Task {
word_count: number; word_count: number;
meta_title?: string | null; meta_title?: string | null;
meta_description?: 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; assigned_post_id?: number | null;
post_url?: string | null; post_url?: string | null;
created_at: string; created_at: string;