autoamtiona nd other pages udpates,
This commit is contained in:
@@ -152,10 +152,12 @@ class AutomationService:
|
|||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
# Query pending keywords
|
# Query pending keywords
|
||||||
|
# FIXED: Match pipeline_overview query - use status='new' only
|
||||||
|
# Keywords with status='new' are ready for clustering, regardless of cluster FK
|
||||||
|
# (If cluster FK is set but status='new', it means the old cluster was deleted)
|
||||||
pending_keywords = Keywords.objects.filter(
|
pending_keywords = Keywords.objects.filter(
|
||||||
site=self.site,
|
site=self.site,
|
||||||
status='new',
|
status='new',
|
||||||
cluster__isnull=True,
|
|
||||||
disabled=False
|
disabled=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -411,10 +413,10 @@ class AutomationService:
|
|||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
# ADDED: Pre-stage validation - verify Stage 1 completion
|
# ADDED: Pre-stage validation - verify Stage 1 completion
|
||||||
|
# FIXED: Match pipeline_overview query - use status='new' only
|
||||||
pending_keywords = Keywords.objects.filter(
|
pending_keywords = Keywords.objects.filter(
|
||||||
site=self.site,
|
site=self.site,
|
||||||
status='new',
|
status='new',
|
||||||
cluster__isnull=True,
|
|
||||||
disabled=False
|
disabled=False
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
@@ -1138,6 +1140,26 @@ class AutomationService:
|
|||||||
self.run.run_id, self.account.id, self.site.id,
|
self.run.run_id, self.account.id, self.site.id,
|
||||||
stage_number, f"Content '{content.title}' complete ({content_processed}/{total_content})"
|
stage_number, f"Content '{content.title}' complete ({content_processed}/{total_content})"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ADDED: Incremental save after each content piece for real-time frontend progress
|
||||||
|
# This allows the frontend to show accurate progress during Stage 5
|
||||||
|
current_prompts_created = Images.objects.filter(
|
||||||
|
site=self.site,
|
||||||
|
status='pending',
|
||||||
|
created_at__gte=self.run.started_at
|
||||||
|
).count()
|
||||||
|
current_credits_used = self._get_credits_used() - credits_before
|
||||||
|
current_time_elapsed = self._format_time_elapsed(start_time)
|
||||||
|
self.run.stage_5_result = {
|
||||||
|
'content_processed': content_processed,
|
||||||
|
'content_total': total_content,
|
||||||
|
'prompts_created': current_prompts_created,
|
||||||
|
'credits_used': current_credits_used,
|
||||||
|
'time_elapsed': current_time_elapsed,
|
||||||
|
'in_progress': True
|
||||||
|
}
|
||||||
|
self.run.save(update_fields=['stage_5_result'])
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# FIXED: Log error but continue processing remaining content
|
# FIXED: Log error but continue processing remaining content
|
||||||
error_msg = f"Failed to extract prompts for content '{content.title}': {str(e)}"
|
error_msg = f"Failed to extract prompts for content '{content.title}': {str(e)}"
|
||||||
@@ -1441,9 +1463,14 @@ class AutomationService:
|
|||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
|
|
||||||
def run_stage_7(self):
|
def run_stage_7(self):
|
||||||
"""Stage 7: Manual Review Gate (Count Only)"""
|
"""Stage 7: Auto-Approve and Publish Review Content
|
||||||
|
|
||||||
|
This stage automatically approves content in 'review' status and
|
||||||
|
marks it as 'published' (or queues for WordPress sync).
|
||||||
|
"""
|
||||||
stage_number = 7
|
stage_number = 7
|
||||||
stage_name = "Manual Review Gate"
|
stage_name = "Review → Published"
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
# Query content ready for review
|
# Query content ready for review
|
||||||
ready_for_review = Content.objects.filter(
|
ready_for_review = Content.objects.filter(
|
||||||
@@ -1452,7 +1479,6 @@ class AutomationService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
total_count = ready_for_review.count()
|
total_count = ready_for_review.count()
|
||||||
content_ids = list(ready_for_review.values_list('id', flat=True))
|
|
||||||
|
|
||||||
# Log stage start
|
# Log stage start
|
||||||
self.logger.log_stage_start(
|
self.logger.log_stage_start(
|
||||||
@@ -1460,22 +1486,129 @@ class AutomationService:
|
|||||||
stage_number, stage_name, total_count
|
stage_number, stage_name, total_count
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if total_count == 0:
|
||||||
self.logger.log_stage_progress(
|
self.logger.log_stage_progress(
|
||||||
self.run.run_id, self.account.id, self.site.id,
|
self.run.run_id, self.account.id, self.site.id,
|
||||||
stage_number, f"Automation complete. {total_count} content pieces ready for review"
|
stage_number, "No content in review to approve - completing automation"
|
||||||
)
|
)
|
||||||
|
self.run.stage_7_result = {
|
||||||
|
'ready_for_review': 0,
|
||||||
|
'approved_count': 0,
|
||||||
|
'content_ids': []
|
||||||
|
}
|
||||||
|
self.run.status = 'completed'
|
||||||
|
self.run.completed_at = datetime.now()
|
||||||
|
self.run.save()
|
||||||
|
cache.delete(f'automation_lock_{self.site.id}')
|
||||||
|
return
|
||||||
|
|
||||||
if content_ids:
|
content_list = list(ready_for_review)
|
||||||
self.logger.log_stage_progress(
|
approved_count = 0
|
||||||
self.run.run_id, self.account.id, self.site.id,
|
|
||||||
stage_number, f"Content IDs ready: {content_ids[:10]}..." if len(content_ids) > 10 else f"Content IDs ready: {content_ids}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Save results
|
# INITIAL SAVE: Set totals immediately
|
||||||
self.run.stage_7_result = {
|
self.run.stage_7_result = {
|
||||||
'ready_for_review': total_count,
|
'ready_for_review': total_count,
|
||||||
'content_ids': content_ids
|
'review_total': total_count,
|
||||||
|
'approved_count': 0,
|
||||||
|
'content_ids': [],
|
||||||
|
'in_progress': True
|
||||||
}
|
}
|
||||||
|
self.run.save(update_fields=['stage_7_result'])
|
||||||
|
|
||||||
|
for idx, content in enumerate(content_list, 1):
|
||||||
|
# Check if automation should stop (paused or cancelled)
|
||||||
|
should_stop, reason = self._check_should_stop()
|
||||||
|
if should_stop:
|
||||||
|
self.logger.log_stage_progress(
|
||||||
|
self.run.run_id, self.account.id, self.site.id,
|
||||||
|
stage_number, f"Stage {reason} - saving progress ({approved_count} content approved)"
|
||||||
|
)
|
||||||
|
time_elapsed = self._format_time_elapsed(start_time)
|
||||||
|
self.run.stage_7_result = {
|
||||||
|
'ready_for_review': total_count,
|
||||||
|
'review_total': total_count,
|
||||||
|
'approved_count': approved_count,
|
||||||
|
'content_ids': list(Content.objects.filter(
|
||||||
|
site=self.site, status='published', updated_at__gte=self.run.started_at
|
||||||
|
).values_list('id', flat=True)),
|
||||||
|
'partial': True,
|
||||||
|
'stopped_reason': reason,
|
||||||
|
'time_elapsed': time_elapsed
|
||||||
|
}
|
||||||
|
self.run.save()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.logger.log_stage_progress(
|
||||||
|
self.run.run_id, self.account.id, self.site.id,
|
||||||
|
stage_number, f"Approving content {idx}/{total_count}: {content.title}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Approve content by changing status to 'published'
|
||||||
|
content.status = 'published'
|
||||||
|
content.save(update_fields=['status', 'updated_at'])
|
||||||
|
|
||||||
|
approved_count += 1
|
||||||
|
|
||||||
|
self.logger.log_stage_progress(
|
||||||
|
self.run.run_id, self.account.id, self.site.id,
|
||||||
|
stage_number, f"Content '{content.title}' approved ({approved_count}/{total_count})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Incremental save for real-time frontend progress
|
||||||
|
current_time_elapsed = self._format_time_elapsed(start_time)
|
||||||
|
self.run.stage_7_result = {
|
||||||
|
'ready_for_review': total_count,
|
||||||
|
'review_total': total_count,
|
||||||
|
'approved_count': approved_count,
|
||||||
|
'content_ids': [], # Don't store full list during processing
|
||||||
|
'time_elapsed': current_time_elapsed,
|
||||||
|
'in_progress': True
|
||||||
|
}
|
||||||
|
self.run.save(update_fields=['stage_7_result'])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Failed to approve content '{content.title}': {str(e)}"
|
||||||
|
logger.error(f"[AutomationService] {error_msg}", exc_info=True)
|
||||||
|
self.logger.log_stage_error(
|
||||||
|
self.run.run_id, self.account.id, self.site.id,
|
||||||
|
stage_number, error_msg
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Small delay between approvals to prevent overwhelming the system
|
||||||
|
if idx < total_count:
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Final results
|
||||||
|
time_elapsed = self._format_time_elapsed(start_time)
|
||||||
|
content_ids = list(Content.objects.filter(
|
||||||
|
site=self.site,
|
||||||
|
status='published',
|
||||||
|
updated_at__gte=self.run.started_at
|
||||||
|
).values_list('id', flat=True))
|
||||||
|
|
||||||
|
self.logger.log_stage_complete(
|
||||||
|
self.run.run_id, self.account.id, self.site.id,
|
||||||
|
stage_number, approved_count, time_elapsed, 0
|
||||||
|
)
|
||||||
|
|
||||||
|
self.run.stage_7_result = {
|
||||||
|
'ready_for_review': total_count,
|
||||||
|
'review_total': total_count,
|
||||||
|
'approved_count': approved_count,
|
||||||
|
'content_ids': content_ids,
|
||||||
|
'time_elapsed': time_elapsed,
|
||||||
|
'in_progress': False
|
||||||
|
}
|
||||||
|
self.run.status = 'completed'
|
||||||
|
self.run.completed_at = datetime.now()
|
||||||
|
self.run.save()
|
||||||
|
|
||||||
|
# Release lock
|
||||||
|
cache.delete(f'automation_lock_{self.site.id}')
|
||||||
|
|
||||||
|
logger.info(f"[AutomationService] Stage 7 complete: {approved_count} content pieces approved and published")
|
||||||
self.run.status = 'completed'
|
self.run.status = 'completed'
|
||||||
self.run.completed_at = datetime.now()
|
self.run.completed_at = datetime.now()
|
||||||
self.run.save()
|
self.run.save()
|
||||||
@@ -1501,8 +1634,8 @@ class AutomationService:
|
|||||||
|
|
||||||
def estimate_credits(self) -> int:
|
def estimate_credits(self) -> int:
|
||||||
"""Estimate total credits needed for automation"""
|
"""Estimate total credits needed for automation"""
|
||||||
# Count items
|
# Count items - FIXED: Match pipeline_overview query
|
||||||
keywords_count = Keywords.objects.filter(site=self.site, status='new', cluster__isnull=True, disabled=False).count()
|
keywords_count = Keywords.objects.filter(site=self.site, status='new', disabled=False).count()
|
||||||
clusters_count = Clusters.objects.filter(site=self.site, status='new').exclude(ideas__isnull=False).count()
|
clusters_count = Clusters.objects.filter(site=self.site, status='new').exclude(ideas__isnull=False).count()
|
||||||
ideas_count = ContentIdeas.objects.filter(site=self.site, status='new').count()
|
ideas_count = ContentIdeas.objects.filter(site=self.site, status='new').count()
|
||||||
tasks_count = Tasks.objects.filter(site=self.site, status='queued').count()
|
tasks_count = Tasks.objects.filter(site=self.site, status='queued').count()
|
||||||
@@ -1526,8 +1659,9 @@ class AutomationService:
|
|||||||
This snapshot is used to calculate global progress percentage correctly.
|
This snapshot is used to calculate global progress percentage correctly.
|
||||||
"""
|
"""
|
||||||
# Stage 1: Keywords pending clustering
|
# Stage 1: Keywords pending clustering
|
||||||
|
# FIXED: Match pipeline_overview query - use status='new' only
|
||||||
stage_1_initial = Keywords.objects.filter(
|
stage_1_initial = Keywords.objects.filter(
|
||||||
site=self.site, status='new', cluster__isnull=True, disabled=False
|
site=self.site, status='new', disabled=False
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
# Stage 2: Clusters needing ideas
|
# Stage 2: Clusters needing ideas
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`rounded-xl p-5 mb-6 border-2 transition-all ${
|
<div className={`rounded-2xl p-5 mb-6 border-2 transition-all ${
|
||||||
isPaused
|
isPaused
|
||||||
? 'bg-gradient-to-r from-warning-50 to-warning-100 dark:from-warning-900/20 dark:to-warning-800/20 border-warning-400'
|
? 'bg-gradient-to-r from-warning-50 to-warning-100 dark:from-warning-900/20 dark:to-warning-800/20 border-warning-400'
|
||||||
: 'bg-gradient-to-r from-brand-50 to-brand-100 dark:from-brand-900/20 dark:to-brand-800/20 border-brand-400'
|
: 'bg-gradient-to-r from-brand-50 to-brand-100 dark:from-brand-900/20 dark:to-brand-800/20 border-brand-400'
|
||||||
@@ -199,24 +199,26 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
|||||||
{/* Header Row */}
|
{/* Header Row */}
|
||||||
<div className="flex items-start justify-between mb-4">
|
<div className="flex items-start justify-between mb-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={`size-10 rounded-lg flex items-center justify-center shadow-md ${
|
<div className={`size-12 rounded-xl flex items-center justify-center shadow-lg ${
|
||||||
isPaused
|
isPaused
|
||||||
? 'bg-gradient-to-br from-warning-500 to-warning-600'
|
? 'bg-gradient-to-br from-warning-500 to-warning-600'
|
||||||
: 'bg-gradient-to-br from-brand-500 to-brand-600'
|
: 'bg-gradient-to-br from-brand-500 to-brand-600'
|
||||||
}`}>
|
}`}>
|
||||||
{isPaused ? (
|
{isPaused ? (
|
||||||
<PauseIcon className="w-5 h-5 text-white" />
|
<PauseIcon className="w-6 h-6 text-white" />
|
||||||
) : (
|
) : (
|
||||||
<BoltIcon className="w-5 h-5 text-white animate-pulse" />
|
<BoltIcon className="w-6 h-6 text-white animate-pulse" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-bold text-gray-900 dark:text-white">
|
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||||
{isPaused ? 'Automation Paused' : 'Automation In Progress'}
|
{isPaused ? 'Automation Paused' : 'Automation In Progress'}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<span className={`font-semibold ${isPaused ? 'text-warning-700' : 'text-brand-700'}`}>
|
||||||
Stage {currentRun.current_stage}: {stageName}
|
Stage {currentRun.current_stage}: {stageName}
|
||||||
<span className={`ml-2 px-1.5 py-0.5 rounded text-xs ${
|
</span>
|
||||||
|
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||||
isPaused ? 'bg-warning-200 text-warning-800' : 'bg-brand-200 text-brand-800'
|
isPaused ? 'bg-warning-200 text-warning-800' : 'bg-brand-200 text-brand-800'
|
||||||
}`}>
|
}`}>
|
||||||
{stageOverview?.type || 'AI'}
|
{stageOverview?.type || 'AI'}
|
||||||
@@ -225,13 +227,14 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Close and Actions */}
|
{/* Close Button - Top Right */}
|
||||||
<div className="flex items-center gap-2">
|
<button
|
||||||
<Button variant="ghost" size="sm" onClick={onClose}>
|
onClick={onClose}
|
||||||
<XMarkIcon className="w-4 h-4" />
|
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors p-1"
|
||||||
<span className="ml-1">Close</span>
|
title="Close"
|
||||||
</Button>
|
>
|
||||||
</div>
|
<XMarkIcon className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress Section */}
|
{/* Progress Section */}
|
||||||
@@ -241,37 +244,38 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
|||||||
{/* Progress Text */}
|
{/* Progress Text */}
|
||||||
<div className="flex items-baseline justify-between mb-2">
|
<div className="flex items-baseline justify-between mb-2">
|
||||||
<div className="flex items-baseline gap-3">
|
<div className="flex items-baseline gap-3">
|
||||||
<span className={`text-4xl font-bold ${isPaused ? 'text-warning-600' : 'text-brand-600'}`}>
|
<span className={`text-5xl font-bold ${isPaused ? 'text-warning-600' : 'text-brand-600'}`}>
|
||||||
{displayPercent}%
|
{displayPercent}%
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-base font-medium text-gray-600">
|
||||||
{realProcessed}/{realTotal} {outputLabel}
|
{realProcessed}/{realTotal} {outputLabel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-400">
|
<span className="text-sm font-medium text-gray-500">
|
||||||
{realTotal - realProcessed} remaining
|
{realTotal - realProcessed} remaining
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress Bar */}
|
{/* Progress Bar - Vibrant */}
|
||||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3 overflow-hidden">
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-4 overflow-hidden shadow-inner">
|
||||||
<div
|
<div
|
||||||
className={`h-3 rounded-full transition-all duration-300 ${
|
className={`h-4 rounded-full transition-all duration-300 shadow-sm ${
|
||||||
isPaused
|
isPaused
|
||||||
? 'bg-gradient-to-r from-warning-400 to-warning-600'
|
? 'bg-gradient-to-r from-warning-400 via-warning-500 to-warning-600'
|
||||||
: 'bg-gradient-to-r from-brand-400 to-brand-600'
|
: 'bg-gradient-to-r from-brand-400 via-brand-500 to-brand-600'
|
||||||
} ${!isPaused && displayPercent < 100 ? 'animate-pulse' : ''}`}
|
} ${!isPaused && displayPercent < 100 ? 'animate-pulse' : ''}`}
|
||||||
style={{ width: `${Math.min(displayPercent, 100)}%` }}
|
style={{ width: `${Math.min(displayPercent, 100)}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Control Buttons */}
|
{/* Control Buttons - Clean Design */}
|
||||||
<div className="flex items-center gap-3 mt-4">
|
<div className="flex items-center gap-3 mt-4">
|
||||||
{currentRun.status === 'running' ? (
|
{currentRun.status === 'running' ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={handlePause}
|
onClick={handlePause}
|
||||||
disabled={isPausing}
|
disabled={isPausing}
|
||||||
variant="secondary"
|
variant="outline"
|
||||||
|
tone="warning"
|
||||||
size="sm"
|
size="sm"
|
||||||
startIcon={<PauseIcon className="w-4 h-4" />}
|
startIcon={<PauseIcon className="w-4 h-4" />}
|
||||||
>
|
>
|
||||||
@@ -282,6 +286,7 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
|||||||
onClick={handleResume}
|
onClick={handleResume}
|
||||||
disabled={isResuming}
|
disabled={isResuming}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
tone="success"
|
||||||
size="sm"
|
size="sm"
|
||||||
startIcon={<PlayIcon className="w-4 h-4" />}
|
startIcon={<PlayIcon className="w-4 h-4" />}
|
||||||
>
|
>
|
||||||
@@ -289,43 +294,44 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
disabled={isCancelling}
|
disabled={isCancelling}
|
||||||
className="text-sm text-gray-500 hover:text-error-600 transition-colors"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
<XMarkIcon className="w-4 h-4 inline mr-1" />
|
|
||||||
{isCancelling ? 'Cancelling...' : 'Cancel'}
|
{isCancelling ? 'Cancelling...' : 'Cancel'}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Metrics - 30% width */}
|
{/* Metrics - 30% width */}
|
||||||
<div className="w-44 space-y-2">
|
<div className="w-44 space-y-2">
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg px-3 py-2 border border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
<div className="bg-white dark:bg-gray-800 rounded-xl px-3 py-2.5 border border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<ClockIcon className="w-4 h-4 text-gray-400" />
|
<ClockIcon className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-xs text-gray-500 uppercase">Duration</span>
|
<span className="text-xs font-medium text-gray-500 uppercase">Duration</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-bold text-gray-900 dark:text-white">{formatDuration(currentRun.started_at)}</span>
|
<span className="text-base font-bold text-gray-900 dark:text-white">{formatDuration(currentRun.started_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg px-3 py-2 border border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
<div className="bg-white dark:bg-gray-800 rounded-xl px-3 py-2.5 border border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<BoltIcon className="w-4 h-4 text-warning-500" />
|
<BoltIcon className="w-4 h-4 text-warning-500" />
|
||||||
<span className="text-xs text-gray-500 uppercase">Credits</span>
|
<span className="text-xs font-medium text-gray-500 uppercase">Credits</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-bold text-warning-600">{currentRun.total_credits_used}</span>
|
<span className="text-base font-bold text-warning-600">{currentRun.total_credits_used}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg px-3 py-2 border border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
<div className="bg-white dark:bg-gray-800 rounded-xl px-3 py-2.5 border border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||||
<span className="text-xs text-gray-500 uppercase">Stage</span>
|
<span className="text-xs font-medium text-gray-500 uppercase">Stage</span>
|
||||||
<span className="text-sm font-bold text-gray-900 dark:text-white">{currentRun.current_stage} of 7</span>
|
<span className="text-base font-bold text-gray-900 dark:text-white">{currentRun.current_stage} of 7</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg px-3 py-2 border border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
<div className="bg-white dark:bg-gray-800 rounded-xl px-3 py-2.5 border border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||||
<span className="text-xs text-gray-500 uppercase">Status</span>
|
<span className="text-xs font-medium text-gray-500 uppercase">Status</span>
|
||||||
<span className={`text-sm font-bold ${isPaused ? 'text-warning-600' : 'text-success-600'}`}>
|
<span className={`text-base font-bold ${isPaused ? 'text-warning-600' : 'text-success-600'}`}>
|
||||||
{isPaused ? 'Paused' : 'Running'}
|
{isPaused ? 'Paused' : 'Running'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import React from 'react';
|
|||||||
import { AutomationRun, InitialSnapshot, StageProgress, GlobalProgress } from '../../services/automationService';
|
import { AutomationRun, InitialSnapshot, StageProgress, GlobalProgress } from '../../services/automationService';
|
||||||
import { BoltIcon, CheckCircleIcon, PauseIcon } from '../../icons';
|
import { BoltIcon, CheckCircleIcon, PauseIcon } from '../../icons';
|
||||||
|
|
||||||
// Stage colors matching AutomationPage STAGE_CONFIG
|
// Stage colors matching AutomationPage STAGE_CONFIG exactly
|
||||||
const STAGE_COLORS = [
|
const STAGE_COLORS = [
|
||||||
'from-brand-500 to-brand-600', // Stage 1: Keywords → Clusters
|
'from-brand-500 to-brand-600', // Stage 1: Keywords → Clusters (brand/teal)
|
||||||
'from-purple-500 to-purple-600', // Stage 2: Clusters → Ideas
|
'from-purple-500 to-purple-600', // Stage 2: Clusters → Ideas (purple)
|
||||||
'from-purple-500 to-purple-600', // Stage 3: Ideas → Tasks
|
'from-warning-500 to-warning-600', // Stage 3: Ideas → Tasks (amber)
|
||||||
'from-success-500 to-success-600', // Stage 4: Tasks → Content
|
'from-brand-500 to-brand-600', // Stage 4: Tasks → Content (brand/teal)
|
||||||
'from-warning-500 to-warning-600', // Stage 5: Content → Image Prompts
|
'from-success-500 to-success-600', // Stage 5: Content → Image Prompts (green)
|
||||||
'from-purple-500 to-purple-600', // Stage 6: Image Prompts → Images
|
'from-purple-500 to-purple-600', // Stage 6: Image Prompts → Images (purple)
|
||||||
'from-success-500 to-success-600', // Stage 7: Manual Review Gate
|
'from-success-500 to-success-600', // Stage 7: Review Gate (green)
|
||||||
];
|
];
|
||||||
|
|
||||||
const STAGE_NAMES = [
|
const STAGE_NAMES = [
|
||||||
@@ -25,7 +25,7 @@ const STAGE_NAMES = [
|
|||||||
'Tasks',
|
'Tasks',
|
||||||
'Content',
|
'Content',
|
||||||
'Prompts',
|
'Prompts',
|
||||||
'Review',
|
'Publish',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Helper to get processed count from stage result using correct key
|
// Helper to get processed count from stage result using correct key
|
||||||
@@ -76,13 +76,9 @@ const GlobalProgressBar: React.FC<GlobalProgressBarProps> = ({
|
|||||||
|
|
||||||
// Fallback: Calculate from currentRun and initialSnapshot
|
// Fallback: Calculate from currentRun and initialSnapshot
|
||||||
const snapshot = initialSnapshot || (currentRun as any)?.initial_snapshot;
|
const snapshot = initialSnapshot || (currentRun as any)?.initial_snapshot;
|
||||||
if (!snapshot) {
|
|
||||||
return { percentage: 0, completed: 0, total: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalInitial = snapshot.total_initial_items || 0;
|
// Calculate total completed from all stage results
|
||||||
let totalCompleted = 0;
|
let totalCompleted = 0;
|
||||||
|
|
||||||
for (let i = 1; i <= 7; i++) {
|
for (let i = 1; i <= 7; i++) {
|
||||||
const result = (currentRun as any)[`stage_${i}_result`];
|
const result = (currentRun as any)[`stage_${i}_result`];
|
||||||
if (result) {
|
if (result) {
|
||||||
@@ -90,12 +86,26 @@ const GlobalProgressBar: React.FC<GlobalProgressBarProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const percentage = totalInitial > 0 ? Math.round((totalCompleted / totalInitial) * 100) : 0;
|
// Calculate total items - sum of ALL stage initials from snapshot (updated after each stage)
|
||||||
|
// This accounts for items created during the run (e.g., keywords create clusters, clusters create ideas)
|
||||||
|
let totalItems = 0;
|
||||||
|
if (snapshot) {
|
||||||
|
for (let i = 1; i <= 7; i++) {
|
||||||
|
const stageInitial = snapshot[`stage_${i}_initial`] || 0;
|
||||||
|
totalItems += stageInitial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the updated total from snapshot, or fallback to total_initial_items
|
||||||
|
const finalTotal = totalItems > 0 ? totalItems : (snapshot?.total_initial_items || 0);
|
||||||
|
|
||||||
|
// Ensure completed never exceeds total (clamp percentage to 100%)
|
||||||
|
const percentage = finalTotal > 0 ? Math.round((totalCompleted / finalTotal) * 100) : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
percentage: Math.min(percentage, 100),
|
percentage: Math.min(percentage, 100),
|
||||||
completed: totalCompleted,
|
completed: totalCompleted,
|
||||||
total: totalInitial,
|
total: finalTotal,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,46 +149,46 @@ const GlobalProgressBar: React.FC<GlobalProgressBarProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`
|
<div className={`
|
||||||
rounded-xl p-4 mb-6 border-2 transition-all
|
rounded-2xl p-5 mb-6 border-2 transition-all
|
||||||
${isPaused
|
${isPaused
|
||||||
? 'bg-gradient-to-r from-warning-50 to-warning-100 dark:from-warning-900/20 dark:to-warning-800/20 border-warning-300 dark:border-warning-700'
|
? 'bg-gradient-to-r from-warning-50 to-warning-100 dark:from-warning-900/20 dark:to-warning-800/20 border-warning-300 dark:border-warning-700'
|
||||||
: 'bg-gradient-to-r from-brand-50 to-brand-100 dark:from-brand-900/20 dark:to-brand-800/20 border-brand-300 dark:border-brand-700'
|
: 'bg-gradient-to-r from-brand-50 to-brand-100 dark:from-brand-900/20 dark:to-brand-800/20 border-brand-300 dark:border-brand-700'
|
||||||
}
|
}
|
||||||
`}>
|
`}>
|
||||||
{/* Header Row */}
|
{/* Header Row */}
|
||||||
<div className="flex justify-between items-center mb-3">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={`
|
<div className={`
|
||||||
size-10 rounded-lg flex items-center justify-center shadow-md
|
size-12 rounded-xl flex items-center justify-center shadow-lg
|
||||||
${isPaused
|
${isPaused
|
||||||
? 'bg-gradient-to-br from-warning-500 to-warning-600'
|
? 'bg-gradient-to-br from-warning-500 to-warning-600'
|
||||||
: 'bg-gradient-to-br from-brand-500 to-brand-600'
|
: 'bg-gradient-to-br from-brand-500 to-brand-600'
|
||||||
}
|
}
|
||||||
`}>
|
`}>
|
||||||
{isPaused ? (
|
{isPaused ? (
|
||||||
<PauseIcon className="w-5 h-5 text-white" />
|
<PauseIcon className="w-6 h-6 text-white" />
|
||||||
) : percentage >= 100 ? (
|
) : percentage >= 100 ? (
|
||||||
<CheckCircleIcon className="w-5 h-5 text-white" />
|
<CheckCircleIcon className="w-6 h-6 text-white" />
|
||||||
) : (
|
) : (
|
||||||
<BoltIcon className="w-5 h-5 text-white animate-pulse" />
|
<BoltIcon className="w-6 h-6 text-white animate-pulse" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className={`font-bold ${isPaused ? 'text-warning-800 dark:text-warning-200' : 'text-brand-800 dark:text-brand-200'}`}>
|
<div className={`text-lg font-bold ${isPaused ? 'text-warning-800 dark:text-warning-200' : 'text-brand-800 dark:text-brand-200'}`}>
|
||||||
{isPaused ? 'Pipeline Paused' : 'Full Pipeline Progress'}
|
{isPaused ? 'Pipeline Paused' : 'Full Pipeline Progress'}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
Stage {currentStage} of 7 • {formatDuration()}
|
Stage {currentStage} of 7 • {formatDuration()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`text-3xl font-bold ${isPaused ? 'text-warning-600 dark:text-warning-400' : 'text-brand-600 dark:text-brand-400'}`}>
|
<div className={`text-4xl font-bold ${isPaused ? 'text-warning-600 dark:text-warning-400' : 'text-brand-600 dark:text-brand-400'}`}>
|
||||||
{percentage}%
|
{percentage}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Segmented Progress Bar */}
|
{/* Segmented Progress Bar - Taller & More Vibrant */}
|
||||||
<div className="flex h-4 rounded-full overflow-hidden bg-gray-200 dark:bg-gray-700 gap-0.5 mb-2">
|
<div className="flex h-5 rounded-full overflow-hidden bg-gray-200 dark:bg-gray-700 gap-0.5 mb-3 shadow-inner">
|
||||||
{[1, 2, 3, 4, 5, 6, 7].map(stageNum => {
|
{[1, 2, 3, 4, 5, 6, 7].map(stageNum => {
|
||||||
const status = getStageStatus(stageNum);
|
const status = getStageStatus(stageNum);
|
||||||
const stageColor = STAGE_COLORS[stageNum - 1];
|
const stageColor = STAGE_COLORS[stageNum - 1];
|
||||||
@@ -188,9 +198,9 @@ const GlobalProgressBar: React.FC<GlobalProgressBarProps> = ({
|
|||||||
key={stageNum}
|
key={stageNum}
|
||||||
className={`flex-1 transition-all duration-500 relative group ${
|
className={`flex-1 transition-all duration-500 relative group ${
|
||||||
status === 'completed'
|
status === 'completed'
|
||||||
? `bg-gradient-to-r ${stageColor}`
|
? `bg-gradient-to-r ${stageColor} shadow-sm`
|
||||||
: status === 'active'
|
: status === 'active'
|
||||||
? `bg-gradient-to-r ${stageColor} opacity-60 ${!isPaused ? 'animate-pulse' : ''}`
|
? `bg-gradient-to-r ${stageColor} opacity-70 ${!isPaused ? 'animate-pulse' : ''} shadow-sm`
|
||||||
: 'bg-gray-300 dark:bg-gray-600'
|
: 'bg-gray-300 dark:bg-gray-600'
|
||||||
}`}
|
}`}
|
||||||
title={`Stage ${stageNum}: ${STAGE_NAMES[stageNum - 1]}`}
|
title={`Stage ${stageNum}: ${STAGE_NAMES[stageNum - 1]}`}
|
||||||
@@ -206,20 +216,22 @@ const GlobalProgressBar: React.FC<GlobalProgressBarProps> = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer Row */}
|
{/* Footer Row - Larger Font for Stage Numbers */}
|
||||||
<div className="flex justify-between text-xs text-gray-600 dark:text-gray-400">
|
<div className="flex justify-between items-center text-sm text-gray-700 dark:text-gray-300">
|
||||||
<span>{completed} / {total} items processed</span>
|
<span className="font-medium">{completed} / {total} items processed</span>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-3">
|
||||||
{[1, 2, 3, 4, 5, 6, 7].map(stageNum => {
|
{[1, 2, 3, 4, 5, 6, 7].map(stageNum => {
|
||||||
const status = getStageStatus(stageNum);
|
const status = getStageStatus(stageNum);
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
key={stageNum}
|
key={stageNum}
|
||||||
className={`
|
className={`text-base font-semibold ${
|
||||||
${status === 'completed' ? 'text-success-600 dark:text-success-400 font-medium' : ''}
|
status === 'completed' ? 'text-success-600 dark:text-success-400' : ''
|
||||||
${status === 'active' ? `${isPaused ? 'text-warning-600 dark:text-warning-400' : 'text-brand-600 dark:text-brand-400'} font-bold` : ''}
|
} ${
|
||||||
${status === 'pending' ? 'text-gray-400 dark:text-gray-500' : ''}
|
status === 'active' ? `${isPaused ? 'text-warning-600 dark:text-warning-400' : 'text-brand-600 dark:text-brand-400'}` : ''
|
||||||
`}
|
} ${
|
||||||
|
status === 'pending' ? 'text-gray-400 dark:text-gray-500' : ''
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{stageNum}
|
{stageNum}
|
||||||
{status === 'completed' && '✓'}
|
{status === 'completed' && '✓'}
|
||||||
|
|||||||
@@ -116,11 +116,8 @@ export default function PageHeader({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${className}`}>
|
<div className={`${className}`}>
|
||||||
{/* Title now shown in AppHeader - this component only triggers the context update */}
|
{/* Title and description now shown in AppHeader - this component only triggers the context update */}
|
||||||
{/* Show description if provided - can be used for additional context */}
|
{/* Description rendering removed - visible in AppHeader tooltip or not needed */}
|
||||||
{description && (
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">{description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const STAGE_CONFIG = [
|
|||||||
{ icon: CheckCircleIcon, color: 'from-brand-500 to-brand-600', textColor: 'text-brand-600 dark:text-brand-400', bgColor: 'bg-brand-100 dark:bg-brand-900/30', hoverColor: 'hover:border-brand-500', name: 'Tasks → Content' },
|
{ icon: CheckCircleIcon, color: 'from-brand-500 to-brand-600', textColor: 'text-brand-600 dark:text-brand-400', bgColor: 'bg-brand-100 dark:bg-brand-900/30', hoverColor: 'hover:border-brand-500', name: 'Tasks → Content' },
|
||||||
{ icon: PencilIcon, color: 'from-success-500 to-success-600', textColor: 'text-success-600 dark:text-success-400', bgColor: 'bg-success-100 dark:bg-success-900/30', hoverColor: 'hover:border-success-500', name: 'Content → Image Prompts' },
|
{ icon: PencilIcon, color: 'from-success-500 to-success-600', textColor: 'text-success-600 dark:text-success-400', bgColor: 'bg-success-100 dark:bg-success-900/30', hoverColor: 'hover:border-success-500', name: 'Content → Image Prompts' },
|
||||||
{ icon: FileIcon, color: 'from-purple-500 to-purple-600', textColor: 'text-purple-600 dark:text-purple-400', bgColor: 'bg-purple-100 dark:bg-purple-900/30', hoverColor: 'hover:border-purple-500', name: 'Image Prompts → Images' },
|
{ icon: FileIcon, color: 'from-purple-500 to-purple-600', textColor: 'text-purple-600 dark:text-purple-400', bgColor: 'bg-purple-100 dark:bg-purple-900/30', hoverColor: 'hover:border-purple-500', name: 'Image Prompts → Images' },
|
||||||
{ icon: PaperPlaneIcon, color: 'from-success-500 to-success-600', textColor: 'text-success-600 dark:text-success-400', bgColor: 'bg-success-100 dark:bg-success-900/30', hoverColor: 'hover:border-success-500', name: 'Review Gate' },
|
{ icon: PaperPlaneIcon, color: 'from-success-500 to-success-600', textColor: 'text-success-600 dark:text-success-400', bgColor: 'bg-success-100 dark:bg-success-900/30', hoverColor: 'hover:border-success-500', name: 'Review → Published' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const AutomationPage: React.FC = () => {
|
const AutomationPage: React.FC = () => {
|
||||||
@@ -292,7 +292,22 @@ const AutomationPage: React.FC = () => {
|
|||||||
toast.success('Automation started successfully');
|
toast.success('Automation started successfully');
|
||||||
loadCurrentRun();
|
loadCurrentRun();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(error.response?.data?.error || 'Failed to start automation');
|
const errorMsg = error.response?.data?.error || 'Failed to start automation';
|
||||||
|
// Use informational messages for "no work to do" cases, errors for system/account issues
|
||||||
|
const isInfoMessage =
|
||||||
|
errorMsg.toLowerCase().includes('no keywords') ||
|
||||||
|
errorMsg.toLowerCase().includes('no pending') ||
|
||||||
|
errorMsg.toLowerCase().includes('nothing to process') ||
|
||||||
|
errorMsg.toLowerCase().includes('no items') ||
|
||||||
|
errorMsg.toLowerCase().includes('all stages') ||
|
||||||
|
errorMsg.toLowerCase().includes('minimum') ||
|
||||||
|
errorMsg.toLowerCase().includes('at least');
|
||||||
|
|
||||||
|
if (isInfoMessage) {
|
||||||
|
toast.info(errorMsg);
|
||||||
|
} else {
|
||||||
|
toast.error(errorMsg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -382,7 +397,7 @@ const AutomationPage: React.FC = () => {
|
|||||||
const visible = items.filter(i => i && (i.value !== undefined && i.value !== null));
|
const visible = items.filter(i => i && (i.value !== undefined && i.value !== null));
|
||||||
if (visible.length === 0) {
|
if (visible.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex text-xs mt-1">
|
<div className="flex text-sm mt-2">
|
||||||
<div className="flex-1 text-center"></div>
|
<div className="flex-1 text-center"></div>
|
||||||
<div className="flex-1 text-center"></div>
|
<div className="flex-1 text-center"></div>
|
||||||
<div className="flex-1 text-center"></div>
|
<div className="flex-1 text-center"></div>
|
||||||
@@ -392,11 +407,11 @@ const AutomationPage: React.FC = () => {
|
|||||||
|
|
||||||
if (visible.length === 2) {
|
if (visible.length === 2) {
|
||||||
return (
|
return (
|
||||||
<div className="flex text-xs mt-1 justify-center gap-6">
|
<div className="flex text-sm mt-2 justify-center gap-6">
|
||||||
{visible.map((it, idx) => (
|
{visible.map((it, idx) => (
|
||||||
<div key={idx} className="w-1/3 text-center">
|
<div key={idx} className="w-1/3 text-center">
|
||||||
<span className={`${it.colorCls ?? ''} block`}>{it.label}</span>
|
<span className={`${it.colorCls ?? ''} block font-medium`}>{it.label}</span>
|
||||||
<div className="font-bold text-gray-900 dark:text-white">{Number(it.value) || 0}</div>
|
<div className="text-base font-bold text-gray-900 dark:text-white">{Number(it.value) || 0}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -405,11 +420,11 @@ const AutomationPage: React.FC = () => {
|
|||||||
|
|
||||||
// default to 3 columns (equally spaced)
|
// default to 3 columns (equally spaced)
|
||||||
return (
|
return (
|
||||||
<div className="flex text-xs mt-1">
|
<div className="flex text-sm mt-2">
|
||||||
{items.concat(Array(Math.max(0, 3 - items.length)).fill({ label: '', value: '' })).slice(0,3).map((it, idx) => (
|
{items.concat(Array(Math.max(0, 3 - items.length)).fill({ label: '', value: '' })).slice(0,3).map((it, idx) => (
|
||||||
<div key={idx} className="flex-1 text-center">
|
<div key={idx} className="flex-1 text-center">
|
||||||
<span className={`${it.colorCls ?? ''} block`}>{it.label}</span>
|
<span className={`${it.colorCls ?? ''} block font-medium`}>{it.label}</span>
|
||||||
<div className="font-bold text-gray-900 dark:text-white">{it.value !== undefined && it.value !== null ? Number(it.value) : ''}</div>
|
<div className="text-base font-bold text-gray-900 dark:text-white">{it.value !== undefined && it.value !== null ? Number(it.value) : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -431,7 +446,7 @@ const AutomationPage: React.FC = () => {
|
|||||||
|
|
||||||
{/* Compact Schedule & Controls Panel */}
|
{/* Compact Schedule & Controls Panel */}
|
||||||
{config && (
|
{config && (
|
||||||
<ComponentCard className="border-0 overflow-hidden rounded-2xl bg-gradient-to-br from-brand-600 to-brand-700 [&>div]:!py-1.5 [&>div]:!px-4">
|
<ComponentCard className="border-0 overflow-hidden rounded-2xl bg-gradient-to-br from-brand-600 to-brand-700 [&>div]:!py-3 [&>div]:!px-4">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-4 flex-wrap">
|
<div className="flex items-center gap-4 flex-wrap">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -539,11 +554,11 @@ const AutomationPage: React.FC = () => {
|
|||||||
{/* Keywords */}
|
{/* Keywords */}
|
||||||
<div className="bg-gradient-to-br from-brand-50 to-brand-100 dark:from-brand-900/20 dark:to-brand-800/20 rounded-xl p-4 border-2 border-brand-200 dark:border-brand-800">
|
<div className="bg-gradient-to-br from-brand-50 to-brand-100 dark:from-brand-900/20 dark:to-brand-800/20 rounded-xl p-4 border-2 border-brand-200 dark:border-brand-800">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2">
|
||||||
<div className="size-10 rounded-lg bg-gradient-to-br from-brand-500 to-brand-600 flex items-center justify-center">
|
<div className="size-8 rounded-lg bg-gradient-to-br from-brand-500 to-brand-600 flex items-center justify-center">
|
||||||
<ListIcon className="size-5 text-white" />
|
<ListIcon className="size-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-bold text-brand-900 dark:text-brand-100">Keywords</div>
|
<div className="text-base font-bold text-brand-900 dark:text-brand-100">Keywords</div>
|
||||||
</div>
|
</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const res = getStageResult(1);
|
const res = getStageResult(1);
|
||||||
@@ -571,11 +586,11 @@ const AutomationPage: React.FC = () => {
|
|||||||
{/* Clusters */}
|
{/* Clusters */}
|
||||||
<div className="bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 rounded-xl p-4 border-2 border-purple-200 dark:border-purple-800">
|
<div className="bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 rounded-xl p-4 border-2 border-purple-200 dark:border-purple-800">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2">
|
||||||
<div className="size-10 rounded-lg bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center">
|
<div className="size-8 rounded-lg bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center">
|
||||||
<GroupIcon className="size-5 text-white" />
|
<GroupIcon className="size-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-bold text-purple-900 dark:text-purple-100">Clusters</div>
|
<div className="text-base font-bold text-purple-900 dark:text-purple-100">Clusters</div>
|
||||||
</div>
|
</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const res = getStageResult(2);
|
const res = getStageResult(2);
|
||||||
@@ -603,11 +618,11 @@ const AutomationPage: React.FC = () => {
|
|||||||
{/* Ideas */}
|
{/* Ideas */}
|
||||||
<div className="bg-gradient-to-br from-warning-50 to-warning-100 dark:from-warning-900/20 dark:to-warning-800/20 rounded-xl p-4 border-2 border-warning-200 dark:border-warning-800">
|
<div className="bg-gradient-to-br from-warning-50 to-warning-100 dark:from-warning-900/20 dark:to-warning-800/20 rounded-xl p-4 border-2 border-warning-200 dark:border-warning-800">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2">
|
||||||
<div className="size-10 rounded-lg bg-gradient-to-br from-warning-500 to-warning-600 flex items-center justify-center">
|
<div className="size-8 rounded-lg bg-gradient-to-br from-warning-500 to-warning-600 flex items-center justify-center">
|
||||||
<BoltIcon className="size-5 text-white" />
|
<BoltIcon className="size-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-bold text-warning-900 dark:text-warning-100">Ideas</div>
|
<div className="text-base font-bold text-warning-900 dark:text-warning-100">Ideas</div>
|
||||||
</div>
|
</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const res = getStageResult(3);
|
const res = getStageResult(3);
|
||||||
@@ -637,11 +652,11 @@ const AutomationPage: React.FC = () => {
|
|||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="bg-gradient-to-br from-success-50 to-success-100 dark:from-success-900/20 dark:to-success-800/20 rounded-xl p-4 border-2 border-success-200 dark:border-success-800">
|
<div className="bg-gradient-to-br from-success-50 to-success-100 dark:from-success-900/20 dark:to-success-800/20 rounded-xl p-4 border-2 border-success-200 dark:border-success-800">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2">
|
||||||
<div className="size-10 rounded-lg bg-gradient-to-br from-success-500 to-success-600 flex items-center justify-center">
|
<div className="size-8 rounded-lg bg-gradient-to-br from-success-500 to-success-600 flex items-center justify-center">
|
||||||
<FileTextIcon className="size-5 text-white" />
|
<FileTextIcon className="size-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-bold text-success-900 dark:text-success-100">Content</div>
|
<div className="text-base font-bold text-success-900 dark:text-success-100">Content</div>
|
||||||
</div>
|
</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const res = getStageResult(4);
|
const res = getStageResult(4);
|
||||||
@@ -671,11 +686,11 @@ const AutomationPage: React.FC = () => {
|
|||||||
{/* Images */}
|
{/* Images */}
|
||||||
<div className="bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 rounded-xl p-4 border-2 border-purple-200 dark:border-purple-800">
|
<div className="bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 rounded-xl p-4 border-2 border-purple-200 dark:border-purple-800">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2">
|
||||||
<div className="size-10 rounded-lg bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center">
|
<div className="size-8 rounded-lg bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center">
|
||||||
<FileIcon className="size-5 text-white" />
|
<FileIcon className="size-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-bold text-purple-900 dark:text-purple-100">Images</div>
|
<div className="text-base font-bold text-purple-900 dark:text-purple-100">Images</div>
|
||||||
</div>
|
</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const res = getStageResult(6);
|
const res = getStageResult(6);
|
||||||
@@ -782,7 +797,7 @@ const AutomationPage: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
key={stage.number}
|
key={stage.number}
|
||||||
className={`
|
className={`
|
||||||
relative rounded-xl border-2 p-5 transition-all
|
relative rounded-2xl border-2 p-4 transition-all
|
||||||
${isActive
|
${isActive
|
||||||
? 'border-brand-500 bg-brand-50 dark:bg-brand-500/10 shadow-lg'
|
? 'border-brand-500 bg-brand-50 dark:bg-brand-500/10 shadow-lg'
|
||||||
: isComplete
|
: isComplete
|
||||||
@@ -793,62 +808,73 @@ const AutomationPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{/* Compact Header */}
|
{/* Header Row - Icon, Stage Number, Status on left; Function Name on right */}
|
||||||
<div className="flex items-start justify-between mb-3">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex-1">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<div className="text-sm font-bold text-gray-900 dark:text-white">Stage {stage.number}</div>
|
|
||||||
{isActive && <span className="text-xs px-2 py-0.5 bg-brand-500 text-white rounded-full">● Active</span>}
|
|
||||||
{isComplete && <span className="text-xs px-2 py-0.5 bg-success-500 text-white rounded-full">✓</span>}
|
|
||||||
{!isActive && !isComplete && stage.pending > 0 && <span className="text-xs px-2 py-0.5 bg-gray-400 text-white rounded-full">Ready</span>}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs font-medium text-gray-600 dark:text-gray-400">{stageConfig.name}</div>
|
|
||||||
{stageConfig.description && (
|
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-500 mt-1">{stageConfig.description}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={`size-8 rounded-lg bg-gradient-to-br ${stageConfig.color} flex items-center justify-center shadow-md flex-shrink-0`}>
|
<div className={`size-8 rounded-lg bg-gradient-to-br ${stageConfig.color} flex items-center justify-center shadow-md flex-shrink-0`}>
|
||||||
<StageIcon className="size-4 text-white" />
|
<StageIcon className="size-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-base font-bold text-gray-900 dark:text-white">Stage {stage.number}</span>
|
||||||
|
{isActive && <span className="text-xs px-2 py-0.5 bg-brand-500 text-white rounded-full font-medium">● Active</span>}
|
||||||
|
{isComplete && <span className="text-xs px-2 py-0.5 bg-success-500 text-white rounded-full font-medium">✓</span>}
|
||||||
|
{!isActive && !isComplete && stage.pending > 0 && <span className="text-xs px-2 py-0.5 bg-gray-400 text-white rounded-full font-medium">Ready</span>}
|
||||||
|
</div>
|
||||||
|
{/* Stage Function Name - Right side, larger font */}
|
||||||
|
<div className={`text-sm font-bold ${stageConfig.textColor}`}>{stageConfig.name}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Simplified Queue Metrics: Only Pending & Processed */}
|
{/* Single Row: Pending & Processed - Larger Font */}
|
||||||
<div className="space-y-1.5 text-xs mb-3">
|
<div className="flex justify-between items-center mb-3">
|
||||||
<div className="flex justify-between">
|
<div className="text-center">
|
||||||
<span className="text-gray-600 dark:text-gray-400">Pending:</span>
|
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-0.5">Pending</div>
|
||||||
<span className={`font-bold ${pending > 0 ? stageConfig.textColor : 'text-gray-400 dark:text-gray-500'}`}>
|
<div className={`text-xl font-bold ${pending > 0 ? stageConfig.textColor : 'text-gray-400 dark:text-gray-500'}`}>
|
||||||
{pending}
|
{pending}
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
</div>
|
||||||
<span className="text-gray-600 dark:text-gray-400">Processed:</span>
|
<div className="h-8 w-px bg-gray-200 dark:bg-gray-700"></div>
|
||||||
<span className={`font-bold ${processed > 0 ? 'text-success-600 dark:text-success-400' : 'text-gray-400 dark:text-gray-500'}`}>
|
<div className="text-center">
|
||||||
|
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-0.5">Processed</div>
|
||||||
|
<div className={`text-xl font-bold ${processed > 0 ? 'text-success-600 dark:text-success-400' : 'text-gray-400 dark:text-gray-500'}`}>
|
||||||
{processed}
|
{processed}
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Credits and Duration - only show during/after run */}
|
{/* Credits and Duration - only show during/after run */}
|
||||||
{result && (result.credits_used > 0 || result.time_elapsed) && (
|
{result && (result.credits_used > 0 || result.time_elapsed) && (
|
||||||
<div className="flex justify-between pt-1.5 border-t border-gray-200 dark:border-gray-700 text-[10px]">
|
<div className="flex justify-between items-center py-2 border-t border-gray-200 dark:border-gray-700 text-xs">
|
||||||
{result.credits_used > 0 && (
|
{result.credits_used > 0 && (
|
||||||
<span className="text-warning-600 dark:text-warning-400">{result.credits_used} credits</span>
|
<span className="font-semibold text-warning-600 dark:text-warning-400">{result.credits_used} credits</span>
|
||||||
)}
|
)}
|
||||||
{result.time_elapsed && (
|
{result.time_elapsed && (
|
||||||
<span className="text-gray-500 dark:text-gray-400">{result.time_elapsed}</span>
|
<span className="text-gray-500 dark:text-gray-400">{result.time_elapsed}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress Bar */}
|
{/* Progress Bar with breathing circle indicator */}
|
||||||
{(isActive || isComplete || processed > 0) && (
|
{(isActive || isComplete || processed > 0) && (
|
||||||
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
|
<div className="pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex justify-between text-xs text-gray-600 dark:text-gray-400 mb-1.5">
|
<div className="flex justify-between items-center text-xs font-medium text-gray-600 dark:text-gray-400 mb-1.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<span>Progress</span>
|
<span>Progress</span>
|
||||||
|
{isActive && (
|
||||||
|
<span className="relative flex h-3 w-3">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-brand-400 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex rounded-full h-3 w-3 bg-brand-500"></span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isComplete && (
|
||||||
|
<span className="relative flex h-3 w-3">
|
||||||
|
<span className="relative inline-flex rounded-full h-3 w-3 bg-success-500"></span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<span>{isComplete ? '100' : progressPercent}%</span>
|
<span>{isComplete ? '100' : progressPercent}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5 overflow-hidden shadow-inner">
|
||||||
<div
|
<div
|
||||||
className={`bg-gradient-to-r ${stageConfig.color} h-2 rounded-full transition-all duration-500 ${isActive ? 'animate-pulse' : ''}`}
|
className={`bg-gradient-to-r ${isComplete ? 'from-success-400 to-success-600' : stageConfig.color} h-2.5 rounded-full transition-all duration-500`}
|
||||||
style={{ width: `${isComplete ? 100 : progressPercent}%` }}
|
style={{ width: `${isComplete ? 100 : progressPercent}%` }}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -895,7 +921,7 @@ const AutomationPage: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
key={stage.number}
|
key={stage.number}
|
||||||
className={`
|
className={`
|
||||||
relative rounded-xl border-2 p-5 transition-all
|
relative rounded-2xl border-2 p-4 transition-all
|
||||||
${isActive
|
${isActive
|
||||||
? 'border-brand-500 bg-brand-50 dark:bg-brand-500/10 shadow-lg'
|
? 'border-brand-500 bg-brand-50 dark:bg-brand-500/10 shadow-lg'
|
||||||
: isComplete
|
: isComplete
|
||||||
@@ -906,60 +932,73 @@ const AutomationPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between mb-3">
|
{/* Header Row - Icon, Stage Number, Status on left; Function Name on right */}
|
||||||
<div className="flex-1">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-sm font-bold text-gray-900 dark:text-white">Stage {stage.number}</div>
|
|
||||||
{isActive && <span className="text-xs px-2 py-0.5 bg-brand-500 text-white rounded-full">● Active</span>}
|
|
||||||
{isComplete && <span className="text-xs px-2 py-0.5 bg-success-500 text-white rounded-full">✓</span>}
|
|
||||||
{!isActive && !isComplete && stage.pending > 0 && <span className="text-xs px-2 py-0.5 bg-gray-400 text-white rounded-full">Ready</span>}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs font-medium text-gray-600 dark:text-gray-400">{stageConfig.name}</div>
|
|
||||||
{stageConfig.description && (
|
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-500 mt-1">{stageConfig.description}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={`size-8 rounded-lg bg-gradient-to-br ${stageConfig.color} flex items-center justify-center shadow-md flex-shrink-0`}>
|
<div className={`size-8 rounded-lg bg-gradient-to-br ${stageConfig.color} flex items-center justify-center shadow-md flex-shrink-0`}>
|
||||||
<StageIcon className="size-4 text-white" />
|
<StageIcon className="size-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-base font-bold text-gray-900 dark:text-white">Stage {stage.number}</span>
|
||||||
|
{isActive && <span className="text-xs px-2 py-0.5 bg-brand-500 text-white rounded-full font-medium">● Active</span>}
|
||||||
|
{isComplete && <span className="text-xs px-2 py-0.5 bg-success-500 text-white rounded-full font-medium">✓</span>}
|
||||||
|
{!isActive && !isComplete && stage.pending > 0 && <span className="text-xs px-2 py-0.5 bg-gray-400 text-white rounded-full font-medium">Ready</span>}
|
||||||
|
</div>
|
||||||
|
{/* Stage Function Name - Right side, larger font */}
|
||||||
|
<div className={`text-sm font-bold ${stageConfig.textColor}`}>{stageConfig.name}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Simplified Queue Metrics: Only Pending & Processed */}
|
{/* Single Row: Pending & Processed - Larger Font */}
|
||||||
<div className="space-y-1.5 text-xs mb-3">
|
<div className="flex justify-between items-center mb-3">
|
||||||
<div className="flex justify-between">
|
<div className="text-center">
|
||||||
<span className="text-gray-600 dark:text-gray-400">Pending:</span>
|
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-0.5">Pending</div>
|
||||||
<span className={`font-bold ${pending > 0 ? stageConfig.textColor : 'text-gray-400 dark:text-gray-500'}`}>
|
<div className={`text-xl font-bold ${pending > 0 ? stageConfig.textColor : 'text-gray-400 dark:text-gray-500'}`}>
|
||||||
{pending}
|
{pending}
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
</div>
|
||||||
<span className="text-gray-600 dark:text-gray-400">Processed:</span>
|
<div className="h-8 w-px bg-gray-200 dark:bg-gray-700"></div>
|
||||||
<span className={`font-bold ${processed > 0 ? 'text-success-600 dark:text-success-400' : 'text-gray-400 dark:text-gray-500'}`}>
|
<div className="text-center">
|
||||||
|
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-0.5">Processed</div>
|
||||||
|
<div className={`text-xl font-bold ${processed > 0 ? 'text-success-600 dark:text-success-400' : 'text-gray-400 dark:text-gray-500'}`}>
|
||||||
{processed}
|
{processed}
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Credits and Duration - only show during/after run */}
|
{/* Credits and Duration - only show during/after run */}
|
||||||
{result && (result.credits_used > 0 || result.time_elapsed) && (
|
{result && (result.credits_used > 0 || result.time_elapsed) && (
|
||||||
<div className="flex justify-between pt-1.5 border-t border-gray-200 dark:border-gray-700 text-[10px]">
|
<div className="flex justify-between items-center py-2 border-t border-gray-200 dark:border-gray-700 text-xs">
|
||||||
{result.credits_used > 0 && (
|
{result.credits_used > 0 && (
|
||||||
<span className="text-warning-600 dark:text-warning-400">{result.credits_used} credits</span>
|
<span className="font-semibold text-warning-600 dark:text-warning-400">{result.credits_used} credits</span>
|
||||||
)}
|
)}
|
||||||
{result.time_elapsed && (
|
{result.time_elapsed && (
|
||||||
<span className="text-gray-500 dark:text-gray-400">{result.time_elapsed}</span>
|
<span className="text-gray-500 dark:text-gray-400">{result.time_elapsed}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{/* Progress Bar with breathing circle indicator */}
|
||||||
{(isActive || isComplete || processed > 0) && (
|
{(isActive || isComplete || processed > 0) && (
|
||||||
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
|
<div className="pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex justify-between text-xs text-gray-600 dark:text-gray-400 mb-1.5">
|
<div className="flex justify-between items-center text-xs font-medium text-gray-600 dark:text-gray-400 mb-1.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<span>Progress</span>
|
<span>Progress</span>
|
||||||
|
{isActive && (
|
||||||
|
<span className="relative flex h-3 w-3">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-brand-400 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex rounded-full h-3 w-3 bg-brand-500"></span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isComplete && (
|
||||||
|
<span className="relative flex h-3 w-3">
|
||||||
|
<span className="relative inline-flex rounded-full h-3 w-3 bg-success-500"></span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<span>{isComplete ? '100' : progressPercent}%</span>
|
<span>{isComplete ? '100' : progressPercent}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5 overflow-hidden shadow-inner">
|
||||||
<div
|
<div
|
||||||
className={`bg-gradient-to-r ${stageConfig.color} h-2 rounded-full transition-all duration-500 ${isActive ? 'animate-pulse' : ''}`}
|
className={`bg-gradient-to-r ${isComplete ? 'from-success-400 to-success-600' : stageConfig.color} h-2.5 rounded-full transition-all duration-500`}
|
||||||
style={{ width: `${isComplete ? 100 : progressPercent}%` }}
|
style={{ width: `${isComplete ? 100 : progressPercent}%` }}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -969,62 +1008,119 @@ const AutomationPage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Stage 7 - Manual Review Gate */}
|
{/* Stage 7 - Review → Published (Auto-approve) */}
|
||||||
{pipelineOverview[6] && (() => {
|
{pipelineOverview[6] && (() => {
|
||||||
const stage7 = pipelineOverview[6];
|
const stage7 = pipelineOverview[6];
|
||||||
|
const stageConfig = STAGE_CONFIG[6];
|
||||||
const isActive = currentRun?.current_stage === 7;
|
const isActive = currentRun?.current_stage === 7;
|
||||||
const isComplete = currentRun && currentRun.current_stage > 7;
|
const isComplete = currentRun && currentRun.status === 'completed';
|
||||||
|
const result = currentRun ? (currentRun[`stage_7_result` as keyof AutomationRun] as any) : null;
|
||||||
|
|
||||||
|
// For stage 7: pending = items in review, processed = items approved
|
||||||
|
const approvedCount = result?.approved_count ?? 0;
|
||||||
|
const totalReview = result?.review_total ?? result?.ready_for_review ?? stage7.pending ?? 0;
|
||||||
|
const pendingReview = isActive || isComplete
|
||||||
|
? Math.max(0, totalReview - approvedCount)
|
||||||
|
: stage7.pending ?? 0;
|
||||||
|
|
||||||
|
const progressPercent = totalReview > 0 ? Math.min(Math.round((approvedCount / totalReview) * 100), 100) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
relative rounded-xl border-3 p-5 transition-all
|
relative rounded-2xl border-2 p-4 transition-all
|
||||||
${isActive
|
${isActive
|
||||||
? 'border-warning-500 bg-warning-50 dark:bg-warning-500/10 shadow-lg'
|
? 'border-success-500 bg-success-50 dark:bg-success-500/10 shadow-lg'
|
||||||
: isComplete
|
: isComplete
|
||||||
? 'border-success-500 bg-success-50 dark:bg-success-500/10'
|
? 'border-success-500 bg-success-50 dark:bg-success-500/10'
|
||||||
: stage7.pending > 0
|
: pendingReview > 0
|
||||||
? 'border-warning-300 bg-warning-50 dark:bg-warning-900/20 dark:border-warning-700'
|
? 'border-success-300 bg-success-50 dark:bg-success-900/20 dark:border-success-700'
|
||||||
: 'border-gray-200 bg-gray-50 dark:bg-white/[0.02] dark:border-gray-800'
|
: 'border-gray-200 bg-gray-50 dark:bg-white/[0.02] dark:border-gray-800'
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between mb-3">
|
{/* Header Row - Icon, Stage Number, Status on left; Function Name on right */}
|
||||||
<div className="flex-1">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-sm font-bold text-gray-900 dark:text-white">Stage 7</div>
|
<div className={`size-8 rounded-lg bg-gradient-to-br ${stageConfig.color} flex items-center justify-center shadow-md flex-shrink-0`}>
|
||||||
<span className="text-xs px-2 py-0.5 bg-warning-500 text-white rounded-full">🚫 Stop</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs font-medium text-warning-700 dark:text-warning-300">Manual Review Gate</div>
|
|
||||||
</div>
|
|
||||||
<div className="size-8 rounded-lg bg-gradient-to-br from-warning-500 to-warning-600 flex items-center justify-center shadow-md">
|
|
||||||
<PaperPlaneIcon className="size-4 text-white" />
|
<PaperPlaneIcon className="size-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-base font-bold text-gray-900 dark:text-white">Stage 7</span>
|
||||||
|
{isActive && <span className="text-xs px-2 py-0.5 bg-success-500 text-white rounded-full font-medium">● Active</span>}
|
||||||
|
{isComplete && <span className="text-xs px-2 py-0.5 bg-success-500 text-white rounded-full font-medium">✓</span>}
|
||||||
|
{!isActive && !isComplete && pendingReview > 0 && <span className="text-xs px-2 py-0.5 bg-gray-400 text-white rounded-full font-medium">Ready</span>}
|
||||||
|
</div>
|
||||||
|
{/* Stage Function Name - Right side, larger font */}
|
||||||
|
<div className={`text-sm font-bold ${stageConfig.textColor}`}>{stageConfig.name}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Single Row: Pending & Approved */}
|
||||||
|
<div className="flex justify-between items-center mb-3">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-0.5">Pending</div>
|
||||||
|
<div className={`text-xl font-bold ${pendingReview > 0 ? stageConfig.textColor : 'text-gray-400 dark:text-gray-500'}`}>
|
||||||
|
{pendingReview}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="h-8 w-px bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-0.5">Approved</div>
|
||||||
|
<div className={`text-xl font-bold ${approvedCount > 0 ? 'text-success-600 dark:text-success-400' : 'text-gray-400 dark:text-gray-500'}`}>
|
||||||
|
{approvedCount}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Simplified: Just show the count, no buttons */}
|
{/* Progress Bar with breathing circle indicator */}
|
||||||
<div className="text-center py-4">
|
{(isActive || isComplete || approvedCount > 0) && (
|
||||||
<div className="text-3xl font-bold text-warning-600 dark:text-warning-400">{stage7.pending}</div>
|
<div className="pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||||
<div className="text-xs text-warning-700 dark:text-warning-300 mt-1">ready for review</div>
|
<div className="flex justify-between items-center text-xs font-medium text-gray-600 dark:text-gray-400 mb-1.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>Progress</span>
|
||||||
|
{isActive && (
|
||||||
|
<span className="relative flex h-3 w-3">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-success-400 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex rounded-full h-3 w-3 bg-success-500"></span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isComplete && (
|
||||||
|
<span className="relative flex h-3 w-3">
|
||||||
|
<span className="relative inline-flex rounded-full h-3 w-3 bg-success-500"></span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<span>{isComplete ? '100' : progressPercent}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5 overflow-hidden shadow-inner">
|
||||||
|
<div
|
||||||
|
className={`bg-gradient-to-r ${stageConfig.color} h-2.5 rounded-full transition-all duration-500`}
|
||||||
|
style={{ width: `${isComplete ? 100 : progressPercent}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* Approved summary card (placed after Stage 7 in the same row) */}
|
{/* Approved summary card - Same layout as Stage 7 */}
|
||||||
<div className="rounded-xl p-5 border-2 border-success-200 bg-gradient-to-br from-success-50 to-success-100 dark:from-success-900/10 dark:to-success-800/10 flex flex-col h-full">
|
<div className="rounded-2xl p-4 border-2 border-success-200 bg-gradient-to-br from-success-50 to-success-100 dark:from-success-900/10 dark:to-success-800/10 flex flex-col h-full">
|
||||||
<div className="flex items-center justify-between mb-3">
|
{/* Header Row - Icon and Label on left, Big Count on right */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="size-10 rounded-lg bg-gradient-to-br from-success-500 to-success-600 flex items-center justify-center">
|
<div className="flex items-center gap-2">
|
||||||
<FileTextIcon className="size-5 text-white" />
|
<div className="size-8 rounded-lg bg-gradient-to-br from-success-400 to-success-600 flex items-center justify-center shadow-md flex-shrink-0">
|
||||||
|
<CheckCircleIcon className="size-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-bold text-success-900 dark:text-success-100">Approved</div>
|
<span className="text-base font-bold text-success-900 dark:text-success-100">Approved</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right"> </div>
|
{/* Big count on right */}
|
||||||
|
<div className="text-3xl font-bold text-success-600 dark:text-success-400">
|
||||||
|
{metrics?.content?.published ?? pipelineOverview[3]?.counts?.published ?? getStageResult(4)?.published ?? 0}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex items-center justify-center">
|
|
||||||
<div className="text-4xl md:text-5xl font-extrabold text-success-800 dark:text-success-300">{metrics?.content?.published ?? pipelineOverview[3]?.counts?.published ?? getStageResult(4)?.published ?? 0}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Status Label - Right aligned */}
|
||||||
|
<div className="text-sm font-bold text-success-600 dark:text-success-400 mb-4 text-right">Published Content</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1046,4 +1142,3 @@ const AutomationPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default AutomationPage;
|
export default AutomationPage;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import PageMeta from "../../components/common/PageMeta";
|
import PageMeta from "../../components/common/PageMeta";
|
||||||
|
import PageHeader from "../../components/common/PageHeader";
|
||||||
import { Accordion, AccordionItem } from "../../components/ui/accordion";
|
import { Accordion, AccordionItem } from "../../components/ui/accordion";
|
||||||
import { Card } from "../../components/ui/card";
|
import { Card } from "../../components/ui/card";
|
||||||
import Badge from "../../components/ui/badge/Badge";
|
import Badge from "../../components/ui/badge/Badge";
|
||||||
@@ -9,7 +10,8 @@ import {
|
|||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
ArrowRightIcon,
|
ArrowRightIcon,
|
||||||
FileIcon,
|
FileIcon,
|
||||||
GroupIcon
|
GroupIcon,
|
||||||
|
HelpCircleIcon
|
||||||
} from "../../icons";
|
} from "../../icons";
|
||||||
|
|
||||||
interface TableOfContentsItem {
|
interface TableOfContentsItem {
|
||||||
@@ -188,18 +190,13 @@ export default function Help() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageMeta title="Help Center - IGNY8" description="Guides and tutorials to help you create great content" />
|
<PageMeta title="Help Center - IGNY8" description="Guides and tutorials to help you create great content" />
|
||||||
|
<PageHeader
|
||||||
|
title="Help Center"
|
||||||
|
description="Learn how to use IGNY8 to create and publish amazing content"
|
||||||
|
badge={{ icon: <HelpCircleIcon className="w-4 h-4" />, color: 'blue' }}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
|
||||||
Help Center
|
|
||||||
</h1>
|
|
||||||
<p className="text-lg text-gray-600 dark:text-gray-400">
|
|
||||||
Learn how to use IGNY8 to create and publish amazing content
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Table of Contents */}
|
{/* Table of Contents */}
|
||||||
<Card className="p-6 mb-8 bg-gradient-to-r from-brand-50 to-purple-50 dark:from-brand-900/10 dark:to-purple-900/10 border-brand-200 dark:border-brand-800">
|
<Card className="p-6 mb-8 bg-gradient-to-r from-brand-50 to-purple-50 dark:from-brand-900/10 dark:to-purple-900/10 border-brand-200 dark:border-brand-800">
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Card } from '../../components/ui/card';
|
|||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
import Badge from '../../components/ui/badge/Badge';
|
import Badge from '../../components/ui/badge/Badge';
|
||||||
import PageMeta from '../../components/common/PageMeta';
|
import PageMeta from '../../components/common/PageMeta';
|
||||||
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
import { useAuthStore } from '../../store/authStore';
|
import { useAuthStore } from '../../store/authStore';
|
||||||
import {
|
import {
|
||||||
@@ -265,8 +266,13 @@ export default function AccountSettingsPage() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<>
|
||||||
<PageMeta title="Account Settings" description="Manage your account, profile, and team" />
|
<PageMeta title="Account Settings" description="Manage your account, profile, and team" />
|
||||||
|
<PageHeader
|
||||||
|
title="Account Settings"
|
||||||
|
badge={{ icon: <Settings className="w-4 h-4" />, color: 'blue' }}
|
||||||
|
/>
|
||||||
|
<div className="p-6">
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-brand-500)]" />
|
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-brand-500)]" />
|
||||||
@@ -274,38 +280,28 @@ export default function AccountSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Page titles based on active tab
|
||||||
|
const pageTitles = {
|
||||||
|
account: { title: 'Account Information', description: 'Manage your organization and billing information' },
|
||||||
|
profile: { title: 'Profile Settings', description: 'Update your personal information and preferences' },
|
||||||
|
team: { title: 'Team Management', description: 'Invite and manage team members' },
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<>
|
||||||
<PageMeta title="Account Settings" description="Manage your account, profile, and team" />
|
<PageMeta title="Account Settings" description="Manage your account, profile, and team" />
|
||||||
|
<PageHeader
|
||||||
{/* Page Header with Breadcrumb */}
|
title={pageTitles[activeTab].title}
|
||||||
<div className="mb-6">
|
description={pageTitles[activeTab].description}
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 mb-2">
|
badge={{ icon: <Settings className="w-4 h-4" />, color: 'blue' }}
|
||||||
<span>Account Settings</span>
|
parent="Account Settings"
|
||||||
<span>›</span>
|
/>
|
||||||
<span className="text-gray-900 dark:text-white font-medium">
|
<div className="p-6">
|
||||||
{activeTab === 'account' && 'Account'}
|
|
||||||
{activeTab === 'profile' && 'Profile'}
|
|
||||||
{activeTab === 'team' && 'Team'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
||||||
{activeTab === 'account' && 'Account Information'}
|
|
||||||
{activeTab === 'profile' && 'Profile Settings'}
|
|
||||||
{activeTab === 'team' && 'Team Management'}
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
{activeTab === 'account' && 'Manage your organization and billing information'}
|
|
||||||
{activeTab === 'profile' && 'Update your personal information and preferences'}
|
|
||||||
{activeTab === 'team' && 'Invite and manage team members'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tab Content */}
|
{/* Tab Content */}
|
||||||
<div className="mt-6">
|
|
||||||
{/* Account Tab */}
|
{/* Account Tab */}
|
||||||
{activeTab === 'account' && (
|
{activeTab === 'account' && (
|
||||||
<div className="space-y-6 max-w-4xl">
|
<div className="space-y-6 max-w-4xl">
|
||||||
@@ -762,7 +758,6 @@ export default function AccountSettingsPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Invite Modal */}
|
{/* Invite Modal */}
|
||||||
{showInviteModal && (
|
{showInviteModal && (
|
||||||
@@ -921,5 +916,6 @@ export default function AccountSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
|
import PageMeta from '../../components/common/PageMeta';
|
||||||
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import { useNotificationStore } from '../../store/notificationStore';
|
import { useNotificationStore } from '../../store/notificationStore';
|
||||||
import type { NotificationAPI } from '../../services/notifications.api';
|
import type { NotificationAPI } from '../../services/notifications.api';
|
||||||
import { deleteNotification as deleteNotificationAPI } from '../../services/notifications.api';
|
import { deleteNotification as deleteNotificationAPI } from '../../services/notifications.api';
|
||||||
@@ -177,26 +179,28 @@ export default function NotificationsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<PageMeta title="Notifications" description="View and manage your notifications" />
|
||||||
<title>Notifications - IGNY8</title>
|
<PageHeader
|
||||||
</Helmet>
|
title="Notifications"
|
||||||
|
description="View and manage your notifications"
|
||||||
|
badge={{ icon: <Bell className="w-4 h-4" />, color: 'blue' }}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
{/* Header */}
|
{/* Action Buttons */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
{/* Unread count indicator */}
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
Notifications
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
{unreadCount > 0 ? (
|
{unreadCount > 0 ? (
|
||||||
<span className="font-medium text-brand-600 dark:text-brand-400">
|
<span className="flex items-center gap-2">
|
||||||
{unreadCount} unread notification{unreadCount !== 1 ? 's' : ''}
|
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-brand-500 text-white text-xs font-semibold">
|
||||||
|
{unreadCount}
|
||||||
|
</span>
|
||||||
|
unread notification{unreadCount !== 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
'All caught up!'
|
<span className="text-success-600 dark:text-success-400">✓ All caught up!</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import Badge from '../../components/ui/badge/Badge';
|
import Badge from '../../components/ui/badge/Badge';
|
||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
|
import PageMeta from '../../components/common/PageMeta';
|
||||||
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
import { PricingPlan } from '../../components/ui/pricing-table';
|
import { PricingPlan } from '../../components/ui/pricing-table';
|
||||||
import PricingTable1 from '../../components/ui/pricing-table/pricing-table-1';
|
import PricingTable1 from '../../components/ui/pricing-table/pricing-table-1';
|
||||||
@@ -340,9 +342,18 @@ export default function PlansAndBillingPage() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<>
|
||||||
|
<PageMeta title="Plans & Billing" description="Manage your subscription and billing" />
|
||||||
|
<PageHeader
|
||||||
|
title="Plans & Billing"
|
||||||
|
badge={{ icon: <CreditCard className="w-4 h-4" />, color: 'blue' }}
|
||||||
|
/>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-brand-500)]" />
|
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-brand-500)]" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,20 +376,15 @@ export default function PlansAndBillingPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<PageMeta title="Plans & Billing" description="Manage your subscription and billing" />
|
||||||
|
<PageHeader
|
||||||
|
title={pageTitles[activeTab].title}
|
||||||
|
description={pageTitles[activeTab].description}
|
||||||
|
badge={{ icon: <CreditCard className="w-4 h-4" />, color: 'blue' }}
|
||||||
|
parent="Plans & Billing"
|
||||||
|
/>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
{/* Page Header with Breadcrumb */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 mb-2">
|
|
||||||
<span>Plans & Billing</span>
|
|
||||||
<span>›</span>
|
|
||||||
<span className="text-gray-900 dark:text-white font-medium">{pageTitles[activeTab].title}</span>
|
|
||||||
</div>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">{pageTitles[activeTab].title}</h1>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
{pageTitles[activeTab].description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Activation / pending payment notice */}
|
{/* Activation / pending payment notice */}
|
||||||
{!hasActivePlan && (
|
{!hasActivePlan && (
|
||||||
<div className="mb-4 p-4 rounded-lg border border-warning-200 bg-warning-50 text-warning-800 dark:border-warning-800 dark:bg-warning-900/20 dark:text-warning-200">
|
<div className="mb-4 p-4 rounded-lg border border-warning-200 bg-warning-50 text-warning-800 dark:border-warning-800 dark:bg-warning-900/20 dark:text-warning-200">
|
||||||
@@ -587,14 +593,6 @@ export default function PlansAndBillingPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Upgrade Plans Section */}
|
{/* Upgrade Plans Section */}
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-6">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
{hasActivePlan ? 'Upgrade or Change Your Plan' : 'Choose Your Plan'}
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
Select the plan that best fits your needs
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto" style={{ maxWidth: '1560px' }}>
|
<div className="mx-auto" style={{ maxWidth: '1560px' }}>
|
||||||
<PricingTable1
|
<PricingTable1
|
||||||
title=""
|
title=""
|
||||||
@@ -896,5 +894,6 @@ export default function PlansAndBillingPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4,8 +4,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { AlertCircle, Check, CreditCard, Building2, Wallet, Loader2 } from 'lucide-react';
|
import { AlertCircle, Check, CreditCard, Building2, Wallet, Loader2, Zap } from 'lucide-react';
|
||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
|
import PageMeta from '../../components/common/PageMeta';
|
||||||
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import {
|
import {
|
||||||
getCreditPackages,
|
getCreditPackages,
|
||||||
getAvailablePaymentMethods,
|
getAvailablePaymentMethods,
|
||||||
@@ -142,9 +144,18 @@ export default function PurchaseCreditsPage() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<>
|
||||||
|
<PageMeta title="Purchase Credits" description="Top up your account with credit packages" />
|
||||||
|
<PageHeader
|
||||||
|
title="Purchase Credits"
|
||||||
|
badge={{ icon: <Zap className="w-4 h-4" />, color: 'blue' }}
|
||||||
|
/>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-brand-500)]" />
|
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-brand-500)]" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,9 +163,14 @@ export default function PurchaseCreditsPage() {
|
|||||||
const selectedMethod = paymentMethods.find((m) => m.type === selectedPaymentMethod);
|
const selectedMethod = paymentMethods.find((m) => m.type === selectedPaymentMethod);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8 max-w-2xl">
|
<>
|
||||||
<h1 className="text-3xl font-bold mb-6">Complete Payment</h1>
|
<PageMeta title="Complete Payment" description="Complete your credit purchase" />
|
||||||
|
<PageHeader
|
||||||
|
title="Complete Payment"
|
||||||
|
badge={{ icon: <Zap className="w-4 h-4" />, color: 'blue' }}
|
||||||
|
parent="Purchase Credits"
|
||||||
|
/>
|
||||||
|
<div className="p-6 max-w-2xl">
|
||||||
{/* Invoice Details */}
|
{/* Invoice Details */}
|
||||||
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
||||||
<h2 className="text-xl font-semibold mb-4">Invoice Details</h2>
|
<h2 className="text-xl font-semibold mb-4">Invoice Details</h2>
|
||||||
@@ -286,17 +302,20 @@ export default function PurchaseCreditsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<>
|
||||||
|
<PageMeta title="Purchase Credits" description="Top up your account with credit packages" />
|
||||||
|
<PageHeader
|
||||||
|
title="Purchase Credits"
|
||||||
|
description="Choose a credit package and payment method to top up your account balance."
|
||||||
|
badge={{ icon: <Zap className="w-4 h-4" />, color: 'blue' }}
|
||||||
|
/>
|
||||||
|
<div className="p-6">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
<h1 className="text-3xl font-bold mb-2">Purchase Credits</h1>
|
|
||||||
<p className="text-gray-600 mb-8">
|
|
||||||
Choose a credit package and payment method to top up your account balance.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-error-50 border border-error-200 rounded-lg p-4 mb-6 flex items-start gap-2">
|
<div className="bg-error-50 border border-error-200 rounded-lg p-4 mb-6 flex items-start gap-2">
|
||||||
<AlertCircle className="w-5 h-5 text-error-600 flex-shrink-0 mt-0.5" />
|
<AlertCircle className="w-5 h-5 text-error-600 flex-shrink-0 mt-0.5" />
|
||||||
@@ -426,5 +445,6 @@ export default function PurchaseCreditsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { TrendingUp, Activity, BarChart3, Zap, Calendar } from 'lucide-react';
|
import { TrendingUp, Activity, BarChart3, Zap, Calendar } from 'lucide-react';
|
||||||
import PageMeta from '../../components/common/PageMeta';
|
import PageMeta from '../../components/common/PageMeta';
|
||||||
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
import { getUsageAnalytics, UsageAnalytics, getCreditBalance, type CreditBalance } from '../../services/billing.api';
|
import { getUsageAnalytics, UsageAnalytics, getCreditBalance, type CreditBalance } from '../../services/billing.api';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
@@ -57,8 +58,13 @@ export default function UsageAnalyticsPage() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<>
|
||||||
<PageMeta title="Usage & Analytics" description="Monitor your plan limits and usage" />
|
<PageMeta title="Usage & Analytics" description="Monitor your plan limits and usage" />
|
||||||
|
<PageHeader
|
||||||
|
title="Usage & Analytics"
|
||||||
|
badge={{ icon: <TrendingUp className="w-4 h-4" />, color: 'blue' }}
|
||||||
|
/>
|
||||||
|
<div className="p-6">
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-brand-500"></div>
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-brand-500"></div>
|
||||||
@@ -66,6 +72,7 @@ export default function UsageAnalyticsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,23 +82,22 @@ export default function UsageAnalyticsPage() {
|
|||||||
api: 'Activity Log',
|
api: 'Activity Log',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const tabDescriptions: Record<TabType, string> = {
|
||||||
|
limits: 'See how much you\'re using - Track your credits and content limits',
|
||||||
|
activity: 'See where your credits go - Track credit usage history',
|
||||||
|
api: 'Technical requests - Monitor API activity and usage',
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<>
|
||||||
<PageMeta title="Usage & Analytics" description="Monitor your plan limits and usage" />
|
<PageMeta title="Usage & Analytics" description="Monitor your plan limits and usage" />
|
||||||
|
<PageHeader
|
||||||
{/* Page Header */}
|
title={tabTitles[activeTab]}
|
||||||
<div className="mb-6">
|
description={tabDescriptions[activeTab]}
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400 mb-1">
|
badge={{ icon: <TrendingUp className="w-4 h-4" />, color: 'blue' }}
|
||||||
Usage & Analytics / {tabTitles[activeTab]}
|
parent="Usage & Analytics"
|
||||||
</div>
|
/>
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">{tabTitles[activeTab]}</h1>
|
<div className="p-6">
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
{activeTab === 'limits' && 'See how much you\'re using - Track your credits and content limits'}
|
|
||||||
{activeTab === 'activity' && 'See where your credits go - Track credit usage history'}
|
|
||||||
{activeTab === 'api' && 'Technical requests - Monitor API activity and usage'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Quick Stats Overview */}
|
{/* Quick Stats Overview */}
|
||||||
{creditBalance && (
|
{creditBalance && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||||
@@ -276,5 +282,6 @@ export default function UsageAnalyticsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user