Merge branch 'main' into codex/implement-central-visibility-for-seo-onboarding-tasks

This commit is contained in:
ي
2026-03-08 23:13:08 +05:30
committed by GitHub
13 changed files with 557 additions and 194 deletions

View File

@@ -544,6 +544,7 @@ const App: React.FC = () => {
// Get environment variables with fallbacks
const clerkPublishableKey = process.env.REACT_APP_CLERK_PUBLISHABLE_KEY || '';
const clerkJSUrl = process.env.REACT_APP_CLERK_JS_URL;
// Show error if required keys are missing
if (!clerkPublishableKey) {
@@ -654,7 +655,7 @@ const App: React.FC = () => {
// TODO: Send to error tracking service (Sentry, LogRocket, etc.)
}}
>
<ClerkProvider publishableKey={clerkPublishableKey}>
<ClerkProvider publishableKey={clerkPublishableKey} clerkJSUrl={clerkJSUrl}>
<SubscriptionProvider>
<OnboardingProvider>
{renderApp()}

View File

@@ -14,7 +14,8 @@ import {
Pause,
CheckCircle,
Schedule,
TrendingUp
TrendingUp,
CloudOff
} from '@mui/icons-material';
import { useWorkflowStore } from '../../../stores/workflowStore';
@@ -42,7 +43,9 @@ const WorkflowProgressBar: React.FC<WorkflowProgressBarProps> = ({
startWorkflow,
isWorkflowComplete,
getCompletionPercentage,
generateDailyWorkflow
generateDailyWorkflow,
isDegradedMode,
degradedModeReason
} = useWorkflowStore();
const completionPercentage = getCompletionPercentage();
@@ -79,6 +82,15 @@ const WorkflowProgressBar: React.FC<WorkflowProgressBarProps> = ({
return 'Ready to Start';
};
const getProvenanceLabel = () => {
const summary = currentWorkflow?.provenanceSummary;
if (!summary) return 'Daily Workflow';
if (summary.generationMode === 'agent_committee') return 'Personalized by Agents';
if (summary.generationMode === 'llm_generation' && !summary.fallbackUsed) return 'AI Personalized Guide';
if (summary.fallbackUsed || summary.generationMode === 'controlled_fallback') return 'Baseline Daily Guide';
return 'Daily Workflow';
};
return (
<motion.div
initial={{ opacity: 0, y: -20 }}
@@ -125,6 +137,16 @@ const WorkflowProgressBar: React.FC<WorkflowProgressBarProps> = ({
fontWeight: 600
}}
/>
<Chip
label={getProvenanceLabel()}
size="small"
sx={{
background: 'rgba(255,255,255,0.08)',
color: 'rgba(255,255,255,0.9)',
border: '1px solid rgba(255,255,255,0.2)',
fontWeight: 600
}}
/>
</Box>
{/* Controls */}
@@ -169,6 +191,30 @@ const WorkflowProgressBar: React.FC<WorkflowProgressBarProps> = ({
)}
</Box>
{isDegradedMode && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
mb: 2,
p: 1.5,
borderRadius: 1,
border: `1px solid ${theme.palette.warning.main}55`,
bgcolor: `${theme.palette.warning.main}18`,
}}
>
<CloudOff sx={{ color: theme.palette.warning.light, fontSize: 18 }} />
<Typography variant="body2" sx={{ color: theme.palette.warning.light, fontWeight: 600 }}>
Degraded mode
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.75)' }}>
{degradedModeReason || 'Server workflow is unavailable; local fallback is active.'}
</Typography>
</Box>
)}
{/* Progress Bar */}
<Box sx={{ mb: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>

View File

@@ -311,18 +311,15 @@ class TaskWorkflowOrchestrator {
date: string,
context?: TaskGenerationContext
): Promise<TodayTask[]> {
// This is a placeholder implementation
// In Phase 3, this will be replaced with AI-powered task generation
const defaultTasks: TodayTask[] = [
{
id: `${userId}-${date}-plan-1`,
id: `${userId}-${date}-plan`,
pillarId: 'plan',
title: 'Review content strategy',
description: 'Check and update your content strategy for the week',
title: 'Review today\'s plan',
description: 'Confirm priorities and schedule for today\'s content work.',
status: 'pending',
priority: 'high',
estimatedTime: 15,
estimatedTime: 10,
actionType: 'navigate',
actionUrl: '/content-planning-dashboard',
enabled: true,
@@ -330,29 +327,14 @@ class TaskWorkflowOrchestrator {
color: '#4CAF50'
},
{
id: `${userId}-${date}-plan-2`,
pillarId: 'plan',
title: 'Update content calendar',
description: 'Review and update your content calendar',
id: `${userId}-${date}-generate`,
pillarId: 'generate',
title: 'Generate a draft',
description: 'Create one content draft using the content writer.',
status: 'pending',
priority: 'medium',
estimatedTime: 10,
dependencies: [`${userId}-${date}-plan-1`],
actionType: 'navigate',
actionUrl: '/content-planning-dashboard',
enabled: true,
icon: 'CalendarMonth',
color: '#4CAF50'
},
{
id: `${userId}-${date}-generate-1`,
pillarId: 'generate',
title: 'Create social media content',
description: 'Generate content for your social media platforms',
status: 'pending',
priority: 'high',
estimatedTime: 30,
dependencies: [`${userId}-${date}-plan-1`, `${userId}-${date}-plan-2`],
estimatedTime: 20,
dependencies: [`${userId}-${date}-plan`],
actionType: 'navigate',
actionUrl: '/facebook-writer',
enabled: true,
@@ -360,29 +342,14 @@ class TaskWorkflowOrchestrator {
color: '#2196F3'
},
{
id: `${userId}-${date}-generate-2`,
pillarId: 'generate',
title: 'Create blog content',
description: 'Write blog posts for your website',
status: 'pending',
priority: 'medium',
estimatedTime: 45,
dependencies: [`${userId}-${date}-plan-1`],
actionType: 'navigate',
actionUrl: '/blog-writer',
enabled: true,
icon: 'Article',
color: '#2196F3'
},
{
id: `${userId}-${date}-publish-1`,
id: `${userId}-${date}-publish`,
pillarId: 'publish',
title: 'Publish social media content',
description: 'Publish your created content to social media',
title: 'Publish approved content',
description: 'Open publishing tools and publish today\'s approved draft.',
status: 'pending',
priority: 'medium',
priority: 'high',
estimatedTime: 10,
dependencies: [`${userId}-${date}-generate-1`],
dependencies: [`${userId}-${date}-generate`],
actionType: 'navigate',
actionUrl: '/facebook-writer',
enabled: true,
@@ -390,29 +357,14 @@ class TaskWorkflowOrchestrator {
color: '#FF9800'
},
{
id: `${userId}-${date}-publish-2`,
pillarId: 'publish',
title: 'Publish blog content',
description: 'Publish blog posts to your website',
id: `${userId}-${date}-analyze`,
pillarId: 'analyze',
title: 'Check performance snapshot',
description: 'Review key analytics to assess today\'s published content.',
status: 'pending',
priority: 'medium',
estimatedTime: 15,
dependencies: [`${userId}-${date}-generate-2`],
actionType: 'navigate',
actionUrl: '/blog-writer',
enabled: true,
icon: 'Publish',
color: '#FF9800'
},
{
id: `${userId}-${date}-analyze-1`,
pillarId: 'analyze',
title: 'Review content performance',
description: 'Analyze performance of published content',
status: 'pending',
priority: 'low',
estimatedTime: 20,
dependencies: [`${userId}-${date}-publish-1`, `${userId}-${date}-publish-2`],
estimatedTime: 10,
dependencies: [`${userId}-${date}-publish`],
actionType: 'navigate',
actionUrl: '/analytics-dashboard',
enabled: true,
@@ -420,95 +372,50 @@ class TaskWorkflowOrchestrator {
color: '#9C27B0'
},
{
id: `${userId}-${date}-engage-1`,
id: `${userId}-${date}-engage`,
pillarId: 'engage',
title: 'Respond to comments',
description: 'Engage with comments on your content',
title: 'Respond to audience activity',
description: 'Reply to new comments or mentions from today\'s posts.',
status: 'pending',
priority: 'low',
estimatedTime: 15,
dependencies: [`${userId}-${date}-publish-1`],
estimatedTime: 10,
dependencies: [`${userId}-${date}-publish`],
actionType: 'navigate',
actionUrl: '/engagement-dashboard',
enabled: true,
icon: 'ChatBubbleOutline',
color: '#E91E63'
},
// Engage pillar tasks
{
id: `${userId}-${date}-engage-1`,
pillarId: 'engage',
title: 'Reply to blog comment',
description: 'Respond to comments on your latest blog post',
status: 'pending',
priority: 'high',
estimatedTime: 10,
dependencies: [`${userId}-${date}-analyze-1`],
actionType: 'navigate',
actionUrl: '/engagement-dashboard',
enabled: true,
icon: 'Comment',
color: '#E91E63'
},
{
id: `${userId}-${date}-engage-2`,
pillarId: 'engage',
title: 'Respond to Twitter mention',
description: 'Reply to Twitter mentions and engage with followers',
status: 'pending',
priority: 'medium',
estimatedTime: 5,
dependencies: [`${userId}-${date}-engage-1`],
actionType: 'navigate',
actionUrl: '/engagement-dashboard',
enabled: true,
icon: 'Twitter',
color: '#1DA1F2'
},
// Remarket pillar tasks
{
id: `${userId}-${date}-remarket-1`,
id: `${userId}-${date}-remarket`,
pillarId: 'remarket',
title: 'Launch Retargeting Campaign',
description: 'Create and launch targeted remarketing campaigns',
title: 'Prepare remarketing audience',
description: 'Open remarketing tools to refresh your retargeting audience.',
status: 'pending',
priority: 'high',
estimatedTime: 35,
dependencies: [`${userId}-${date}-engage-2`],
priority: 'low',
estimatedTime: 15,
dependencies: [`${userId}-${date}-analyze`],
actionType: 'navigate',
actionUrl: '/remarketing-dashboard',
enabled: true,
icon: 'Psychology',
color: '#00695C'
},
{
id: `${userId}-${date}-remarket-2`,
pillarId: 'remarket',
title: 'Lead Nurturing Sequence',
description: 'Set up automated lead nurturing workflows',
status: 'pending',
priority: 'medium',
estimatedTime: 30,
dependencies: [`${userId}-${date}-remarket-1`],
actionType: 'navigate',
actionUrl: '/lead-nurturing',
enabled: true,
icon: 'Refresh',
color: '#4CAF50'
}
];
const uniqueTasks = this.ensureUniqueTaskIds(defaultTasks);
// Validate dependencies and get optimal execution order
const tempWorkflow: DailyWorkflow = {
id: `${userId}-${date}`,
date,
userId,
tasks: defaultTasks,
tasks: uniqueTasks,
currentTaskIndex: 0,
completedTasks: 0,
totalTasks: defaultTasks.length,
totalTasks: uniqueTasks.length,
workflowStatus: 'not_started',
totalEstimatedTime: defaultTasks.reduce((sum, task) => sum + task.estimatedTime, 0),
totalEstimatedTime: uniqueTasks.reduce((sum, task) => sum + task.estimatedTime, 0),
actualTimeSpent: 0
};
@@ -517,13 +424,46 @@ class TaskWorkflowOrchestrator {
if (!validation.isValid) {
console.warn('Dependency validation failed:', validation.errors);
// Return tasks without dependencies if validation fails
return defaultTasks.map(task => ({ ...task, dependencies: [] }));
return uniqueTasks.map(task => ({ ...task, dependencies: [] }));
}
// Get optimal execution order
const orderedTasks = taskDependencyManager.getOptimalExecutionOrder(tempWorkflow);
return orderedTasks;
return this.ensureUniqueTaskIds(orderedTasks);
}
private ensureUniqueTaskIds(tasks: TodayTask[]): TodayTask[] {
const idOccurrences = new Map<string, number>();
const oldToNew = new Map<string, string>();
const withUniqueIds = tasks.map(task => {
const count = idOccurrences.get(task.id) ?? 0;
idOccurrences.set(task.id, count + 1);
if (count === 0) {
oldToNew.set(task.id, task.id);
return { ...task };
}
const uniqueId = `${task.id}-${count + 1}`;
oldToNew.set(`${task.id}#${count + 1}`, uniqueId);
return { ...task, id: uniqueId };
});
const allTaskIds = new Set(withUniqueIds.map(task => task.id));
return withUniqueIds.map(task => {
const dependencies = (task.dependencies ?? [])
.map(dep => oldToNew.get(dep) || dep)
.filter((dep, index, arr) => arr.indexOf(dep) === index)
.filter(dep => allTaskIds.has(dep));
return {
...task,
dependencies: dependencies.length > 0 ? dependencies : undefined
};
});
}
/**

View File

@@ -10,10 +10,58 @@ import {
WorkflowError
} from '../types/workflow';
import { taskWorkflowOrchestrator } from '../services/TaskWorkflowOrchestrator';
import { apiClient } from '../api/client';
import { apiClient, ConnectionError, NetworkError } from '../api/client';
const isServerWorkflowId = (workflowId: string) => workflowId.startsWith('daily-');
const normalizeDependencies = (dependencies: unknown): string[] => {
if (Array.isArray(dependencies)) {
return dependencies.map(dep => String(dep).trim()).filter(Boolean);
}
if (typeof dependencies === 'string') {
const raw = dependencies.trim();
if (!raw) return [];
try {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
return parsed.map(dep => String(dep).trim()).filter(Boolean);
}
} catch {}
return [raw];
}
return [];
};
const normalizeServerWorkflow = (workflow: DailyWorkflow): DailyWorkflow => ({
...workflow,
tasks: Array.isArray(workflow.tasks)
? workflow.tasks.map(task => ({
...task,
dependencies: normalizeDependencies(task.dependencies),
}))
: [],
});
const isServerUnavailableError = (error: unknown): boolean =>
error instanceof ConnectionError || error instanceof NetworkError;
const toWorkflowError = (error: unknown, fallbackMessage: string): WorkflowError => {
if (error instanceof WorkflowError) return error;
const message = error instanceof Error ? error.message : fallbackMessage;
return {
code: 'WORKFLOW_ERROR',
message,
timestamp: new Date(),
recoverable: false,
};
};
const computeProgressAndNavigation = (workflow: DailyWorkflow): { progress: WorkflowProgress; navigation: NavigationState } => {
const tasks = Array.isArray(workflow.tasks) ? workflow.tasks : [];
const totalTasks = tasks.length;
@@ -69,6 +117,8 @@ interface WorkflowState {
isWorkflowModalOpen: boolean;
isLoading: boolean;
error: WorkflowError | null;
isDegradedMode: boolean;
degradedModeReason: string | null;
// Actions
generateDailyWorkflow: (userId: string, date?: string) => Promise<void>;
@@ -108,36 +158,71 @@ export const useWorkflowStore = create<WorkflowState>()(
isWorkflowModalOpen: false,
isLoading: false,
error: null,
isDegradedMode: false,
degradedModeReason: null,
// Generate daily workflow
generateDailyWorkflow: async (userId: string, date?: string) => {
set({ isLoading: true, error: null });
try {
try {
const resp = await apiClient.get('/api/today-workflow', { params: date ? { date } : {} });
const serverWorkflow = resp?.data?.data?.workflow as DailyWorkflow | undefined;
if (serverWorkflow && Array.isArray(serverWorkflow.tasks)) {
const derived = computeProgressAndNavigation(serverWorkflow);
set({
currentWorkflow: serverWorkflow,
workflowProgress: derived.progress,
navigationState: derived.navigation,
isLoading: false
});
return;
const resp = await apiClient.get('/api/today-workflow', { params: date ? { date } : {} });
const serverWorkflow = resp?.data?.data?.workflow as DailyWorkflow | undefined;
const planSummary = resp?.data?.data?.plan?.provenance_summary;
if (serverWorkflow && Array.isArray(serverWorkflow.tasks)) {
if (planSummary && !serverWorkflow.provenanceSummary) {
serverWorkflow.provenanceSummary = planSummary;
}
} catch {}
const normalizedWorkflow = normalizeServerWorkflow(serverWorkflow);
const derived = computeProgressAndNavigation(normalizedWorkflow);
set({
currentWorkflow: normalizedWorkflow,
workflowProgress: derived.progress,
navigationState: derived.navigation,
isLoading: false,
isDegradedMode: false,
degradedModeReason: null,
});
return;
}
throw new WorkflowError({
code: 'WORKFLOW_SCHEMA_INVALID',
message: 'Server workflow response is missing a valid tasks array.',
timestamp: new Date(),
recoverable: false,
suggestedAction: 'Refresh and try again. If this persists, contact support.'
});
} catch (error) {
if (!isServerUnavailableError(error)) {
set({
error: toWorkflowError(error, 'Failed to load workflow from server.'),
isLoading: false,
isDegradedMode: false,
degradedModeReason: null,
});
return;
}
}
try {
const workflow = await taskWorkflowOrchestrator.generateDailyWorkflow(userId, date);
const progress = taskWorkflowOrchestrator.getWorkflowProgress(workflow.id);
const navigation = taskWorkflowOrchestrator.getNavigationState(workflow.id);
set({ currentWorkflow: workflow, workflowProgress: progress, navigationState: navigation, isLoading: false });
set({
currentWorkflow: workflow,
workflowProgress: progress,
navigationState: navigation,
isLoading: false,
isDegradedMode: true,
degradedModeReason: 'Server workflow unavailable. Using local fallback workflow.',
error: null,
});
} catch (error) {
const workflowError = error as WorkflowError;
set({
error: workflowError,
isLoading: false
set({
error: toWorkflowError(error, 'Failed to generate local fallback workflow.'),
isLoading: false,
});
}
},

View File

@@ -5,6 +5,14 @@ export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'skipped';
export type TaskPriority = 'high' | 'medium' | 'low';
export type ActionType = 'navigate' | 'modal' | 'external';
export type WorkflowStatus = 'not_started' | 'in_progress' | 'completed' | 'paused' | 'stopped';
export type WorkflowGenerationMode = 'agent_committee' | 'llm_generation' | 'llm_pillar_backfill' | 'controlled_fallback';
export interface WorkflowProvenanceSummary {
generationMode: WorkflowGenerationMode;
committeeAgentCount: number;
fallbackUsed: boolean;
taskSourceBreakdown: Partial<Record<WorkflowGenerationMode, number>>;
}
export interface TodayTask {
id: string;
@@ -44,6 +52,7 @@ export interface DailyWorkflow {
completedAt?: Date;
totalEstimatedTime: number; // in minutes
actualTimeSpent: number; // in minutes
provenanceSummary?: WorkflowProvenanceSummary;
}
export interface WorkflowProgress {
@@ -54,6 +63,7 @@ export interface WorkflowProgress {
nextTask?: TodayTask;
estimatedTimeRemaining: number; // in minutes
actualTimeSpent: number; // in minutes
provenanceSummary?: WorkflowProvenanceSummary;
}
export interface TaskCompletionData {