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:
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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.`,
|
||||||
|
|||||||
@@ -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.`,
|
||||||
|
|||||||
@@ -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.',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user