From 8bb4c5d0160d86ebd56868c4271d511c93790095 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Mon, 10 Nov 2025 13:17:48 +0000 Subject: [PATCH] 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. --- .../igny8_core/modules/writer/serializers.py | 4 + backend/igny8_core/modules/writer/tasks.py | 110 +++++++++++++++--- docs/02-FRONTEND.md | 3 +- frontend/src/App.tsx | 9 +- .../src/components/common/ProgressModal.tsx | 2 +- .../config/pages/bulk-action-modal.config.ts | 2 +- .../src/config/pages/delete-modal.config.ts | 2 +- .../src/config/pages/notifications.config.ts | 2 +- .../src/config/pages/table-actions.config.tsx | 2 +- frontend/src/config/pages/tasks.config.tsx | 86 ++++++++++++++ frontend/src/config/routes.config.ts | 2 +- frontend/src/hooks/useProgressModal.ts | 65 ++++++++--- frontend/src/layout/AppSidebar.tsx | 1 - frontend/src/services/api.ts | 4 + 14 files changed, 247 insertions(+), 47 deletions(-) diff --git a/backend/igny8_core/modules/writer/serializers.py b/backend/igny8_core/modules/writer/serializers.py index da326239..d506bb79 100644 --- a/backend/igny8_core/modules/writer/serializers.py +++ b/backend/igny8_core/modules/writer/serializers.py @@ -30,6 +30,10 @@ class TasksSerializer(serializers.ModelSerializer): 'word_count', 'meta_title', 'meta_description', + 'primary_keyword', + 'secondary_keywords', + 'tags', + 'categories', 'assigned_post_id', 'post_url', 'created_at', diff --git a/backend/igny8_core/modules/writer/tasks.py b/backend/igny8_core/modules/writer/tasks.py index c7a1a718..a7023c10 100644 --- a/backend/igny8_core/modules/writer/tasks.py +++ b/backend/igny8_core/modules/writer/tasks.py @@ -3,6 +3,7 @@ Celery tasks for Writer module - AI content generation """ import logging import re +import time from typing import List from django.db import transaction 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 """ 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 + add_step('INIT', 'success', 'Initializing content generation...', 'request') self.update_state( state='PROGRESS', meta={ @@ -43,7 +66,9 @@ def auto_generate_content_task(self, task_ids: List[int], account_id: int = None 'total': len(task_ids), 'percentage': 0, '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) - # 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( state='PROGRESS', meta={ 'current': 0, 'total': total_tasks, - 'percentage': 2, + 'percentage': 10, '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}") # Don't skip - idea is nullable - # Update progress: Processing task (5-90%) - progress_pct = 5 + int((idx / total_tasks) * 85) + # Update progress: Processing task + # 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( state='PROGRESS', meta={ 'current': idx + 1, 'total': total_tasks, - 'percentage': progress_pct, - 'message': f"Generating content for '{task.title}' ({idx + 1} of {total_tasks})...", - 'phase': 'generating', - 'current_item': task.title + 'percentage': task_progress_pct, + 'message': f"Preparing content generation for '{task.title}' ({idx + 1} of {total_tasks})...", + 'phase': 'PREP', + '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) # 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( state='PROGRESS', meta={ 'current': idx + 1, 'total': total_tasks, - 'percentage': progress_pct, + 'percentage': ai_call_pct, 'message': f"Generating article content for '{task.title}'...", - 'phase': 'generating', - 'current_item': task.title + 'phase': 'AI_CALL', + '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" * 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 logger.info(f" * Normalizing content (length: {len(content)} chars)...") 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)") # 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( state='PROGRESS', meta={ 'current': idx + 1, 'total': total_tasks, - 'percentage': progress_pct, + 'percentage': save_pct, 'message': f"Saving content for '{task.title}' ({word_count} words)...", - 'phase': 'saving', - 'current_item': task.title + 'phase': 'SAVE', + '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() 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 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("=" * 80) - # Final progress update + # Final progress update - mark as DONE final_message = f"Content generation complete: {tasks_updated} articles generated" + add_step('DONE', 'success', final_message, 'response') logger.info("=" * 80) logger.info(f"TASK COMPLETION SUMMARY") 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("=" * 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 { 'success': True, 'tasks_updated': tasks_updated, diff --git a/docs/02-FRONTEND.md b/docs/02-FRONTEND.md index bdbeeeba..dd8c1937 100644 --- a/docs/02-FRONTEND.md +++ b/docs/02-FRONTEND.md @@ -163,8 +163,7 @@ frontend/src/ - `/planner/ideas` - Ideas page - `/writer` - Writer Dashboard - `/writer/tasks` - Tasks page -- `/writer/content` - Content page -- `/writer/drafts` - Drafts page +- `/writer/content` - Drafts page - `/writer/images` - Images page - `/writer/published` - Published page - `/thinker` - Thinker Dashboard diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 81c10d63..b1f960a6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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() { } /> - - - } /> - } /> + } /> diff --git a/frontend/src/components/common/ProgressModal.tsx b/frontend/src/components/common/ProgressModal.tsx index 9c9a3917..49982288 100644 --- a/frontend/src/components/common/ProgressModal.tsx +++ b/frontend/src/components/common/ProgressModal.tsx @@ -146,7 +146,7 @@ export default function ProgressModal({ className="max-w-lg" showCloseButton={status === 'completed' || status === 'error'} > -
+
{/* Header */}
{getStatusIcon()}
diff --git a/frontend/src/config/pages/bulk-action-modal.config.ts b/frontend/src/config/pages/bulk-action-modal.config.ts index 2a19d525..e648879f 100644 --- a/frontend/src/config/pages/bulk-action-modal.config.ts +++ b/frontend/src/config/pages/bulk-action-modal.config.ts @@ -99,7 +99,7 @@ export const bulkActionModalConfigs: Record = { ], }, }, - '/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.`, diff --git a/frontend/src/config/pages/delete-modal.config.ts b/frontend/src/config/pages/delete-modal.config.ts index 589233c9..745d81b4 100644 --- a/frontend/src/config/pages/delete-modal.config.ts +++ b/frontend/src/config/pages/delete-modal.config.ts @@ -40,7 +40,7 @@ export const deleteModalConfigs: Record = { 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.`, diff --git a/frontend/src/config/pages/notifications.config.ts b/frontend/src/config/pages/notifications.config.ts index daeb0b02..3d4950b3 100644 --- a/frontend/src/config/pages/notifications.config.ts +++ b/frontend/src/config/pages/notifications.config.ts @@ -37,7 +37,7 @@ export const pageNotifications: Record = { 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.', diff --git a/frontend/src/config/pages/table-actions.config.tsx b/frontend/src/config/pages/table-actions.config.tsx index d776a446..53998537 100644 --- a/frontend/src/config/pages/table-actions.config.tsx +++ b/frontend/src/config/pages/table-actions.config.tsx @@ -249,7 +249,7 @@ const tableActionsConfigs: Record = { // Removed generate_content from bulk actions - only available as row action ], }, - '/writer/drafts': { + '/writer/content': { rowActions: [ { key: 'edit', diff --git a/frontend/src/config/pages/tasks.config.tsx b/frontend/src/config/pages/tasks.config.tsx index 99a25259..055ddddd 100644 --- a/frontend/src/config/pages/tasks.config.tsx +++ b/frontend/src/config/pages/tasks.config.tsx @@ -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) => ( + + {row.meta_title || row.title || '-'} + + ), }, // 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( + + {row.primary_keyword} + + ); + } + + // 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( + + {keyword} + + ); + } + }); + } + + return keywords.length > 0 ? ( +
+ {keywords} +
+ ) : ( + - + ); + }, + }, + { + 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 ( +
+ {row.tags.map((tag, index) => ( + + {tag} + + ))} +
+ ); + } + return -; + }, + }, + { + 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 ( +
+ {row.categories.map((category, index) => ( + + {category} + + ))} +
+ ); + } + return -; + }, + }, { ...wordCountColumn, sortable: true, diff --git a/frontend/src/config/routes.config.ts b/frontend/src/config/routes.config.ts index 86a9ce40..52d97ee1 100644 --- a/frontend/src/config/routes.config.ts +++ b/frontend/src/config/routes.config.ts @@ -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' }, ], }, diff --git a/frontend/src/hooks/useProgressModal.ts b/frontend/src/hooks/useProgressModal.ts index 02ba3bd7..b23e9018 100644 --- a/frontend/src/hooks/useProgressModal.ts +++ b/frontend/src/hooks/useProgressModal.ts @@ -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 diff --git a/frontend/src/layout/AppSidebar.tsx b/frontend/src/layout/AppSidebar.tsx index 801bad1c..02c9d790 100644 --- a/frontend/src/layout/AppSidebar.tsx +++ b/frontend/src/layout/AppSidebar.tsx @@ -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" }, ], diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 94c8a2cd..5e5502b9 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -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;