Alwrity today's tasks workflow implementation plan.
This commit is contained in:
426
frontend/src/services/TaskCompletionVerifier.ts
Normal file
426
frontend/src/services/TaskCompletionVerifier.ts
Normal file
@@ -0,0 +1,426 @@
|
||||
import {
|
||||
TodayTask
|
||||
} from '../types/workflow';
|
||||
|
||||
interface VerificationResult {
|
||||
isCompleted: boolean;
|
||||
confidence: number; // 0-1 scale
|
||||
evidence: string[];
|
||||
warnings: string[];
|
||||
suggestions: string[];
|
||||
}
|
||||
|
||||
interface VerificationRule {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
pillarId: string;
|
||||
actionType: string;
|
||||
verifier: (task: TodayTask, context?: any) => Promise<VerificationResult>;
|
||||
weight: number; // Importance weight for confidence calculation
|
||||
}
|
||||
|
||||
interface VerificationContext {
|
||||
userId: string;
|
||||
timestamp: Date;
|
||||
platformData?: Record<string, any>;
|
||||
userActivity?: Record<string, any>;
|
||||
}
|
||||
|
||||
class TaskCompletionVerifier {
|
||||
private verificationRules: Map<string, VerificationRule> = new Map();
|
||||
private verificationHistory: Map<string, VerificationResult[]> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.initializeDefaultRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if a task has been completed
|
||||
*/
|
||||
async verifyTaskCompletion(
|
||||
task: TodayTask,
|
||||
context?: VerificationContext
|
||||
): Promise<VerificationResult> {
|
||||
try {
|
||||
const rule = this.verificationRules.get(`${task.pillarId}-${task.actionType}`);
|
||||
|
||||
if (!rule) {
|
||||
// Fallback to basic verification
|
||||
return this.basicVerification(task, context);
|
||||
}
|
||||
|
||||
const result = await rule.verifier(task, context);
|
||||
|
||||
// Store verification history
|
||||
this.storeVerificationResult(task.id, result);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Verification failed for task ${task.id}:`, error);
|
||||
|
||||
return {
|
||||
isCompleted: false,
|
||||
confidence: 0,
|
||||
evidence: [],
|
||||
warnings: [`Verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`],
|
||||
suggestions: ['Try completing the task again or contact support']
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify multiple tasks at once
|
||||
*/
|
||||
async verifyMultipleTasks(
|
||||
tasks: TodayTask[],
|
||||
context?: VerificationContext
|
||||
): Promise<Map<string, VerificationResult>> {
|
||||
const results = new Map<string, VerificationResult>();
|
||||
|
||||
// Verify tasks in parallel for better performance
|
||||
const verificationPromises = tasks.map(async (task) => {
|
||||
const result = await this.verifyTaskCompletion(task, context);
|
||||
results.set(task.id, result);
|
||||
});
|
||||
|
||||
await Promise.all(verificationPromises);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get verification history for a task
|
||||
*/
|
||||
getVerificationHistory(taskId: string): VerificationResult[] {
|
||||
return this.verificationHistory.get(taskId) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom verification rule
|
||||
*/
|
||||
addVerificationRule(rule: VerificationRule): void {
|
||||
const key = `${rule.pillarId}-${rule.actionType}`;
|
||||
this.verificationRules.set(key, rule);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove verification rule
|
||||
*/
|
||||
removeVerificationRule(pillarId: string, actionType: string): void {
|
||||
const key = `${pillarId}-${actionType}`;
|
||||
this.verificationRules.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all verification rules
|
||||
*/
|
||||
getVerificationRules(): VerificationRule[] {
|
||||
return Array.from(this.verificationRules.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize default verification rules
|
||||
*/
|
||||
private initializeDefaultRules(): void {
|
||||
// Plan pillar rules
|
||||
this.addVerificationRule({
|
||||
id: 'plan-navigate',
|
||||
name: 'Content Planning Navigation',
|
||||
description: 'Verify user navigated to content planning dashboard',
|
||||
pillarId: 'plan',
|
||||
actionType: 'navigate',
|
||||
weight: 0.8,
|
||||
verifier: async (task, context) => {
|
||||
return this.verifyNavigation(task, context);
|
||||
}
|
||||
});
|
||||
|
||||
// Generate pillar rules
|
||||
this.addVerificationRule({
|
||||
id: 'generate-navigate',
|
||||
name: 'Content Generation Navigation',
|
||||
description: 'Verify user navigated to content generation tools',
|
||||
pillarId: 'generate',
|
||||
actionType: 'navigate',
|
||||
weight: 0.8,
|
||||
verifier: async (task, context) => {
|
||||
return this.verifyNavigation(task, context);
|
||||
}
|
||||
});
|
||||
|
||||
// Publish pillar rules
|
||||
this.addVerificationRule({
|
||||
id: 'publish-navigate',
|
||||
name: 'Content Publishing Navigation',
|
||||
description: 'Verify user navigated to publishing tools',
|
||||
pillarId: 'publish',
|
||||
actionType: 'navigate',
|
||||
weight: 0.8,
|
||||
verifier: async (task, context) => {
|
||||
return this.verifyNavigation(task, context);
|
||||
}
|
||||
});
|
||||
|
||||
// Analyze pillar rules
|
||||
this.addVerificationRule({
|
||||
id: 'analyze-navigate',
|
||||
name: 'Analytics Navigation',
|
||||
description: 'Verify user navigated to analytics dashboard',
|
||||
pillarId: 'analyze',
|
||||
actionType: 'navigate',
|
||||
weight: 0.8,
|
||||
verifier: async (task, context) => {
|
||||
return this.verifyNavigation(task, context);
|
||||
}
|
||||
});
|
||||
|
||||
// Engage pillar rules
|
||||
this.addVerificationRule({
|
||||
id: 'engage-navigate',
|
||||
name: 'Engagement Navigation',
|
||||
description: 'Verify user navigated to engagement tools',
|
||||
pillarId: 'engage',
|
||||
actionType: 'navigate',
|
||||
weight: 0.8,
|
||||
verifier: async (task, context) => {
|
||||
return this.verifyNavigation(task, context);
|
||||
}
|
||||
});
|
||||
|
||||
// Remarket pillar rules
|
||||
this.addVerificationRule({
|
||||
id: 'remarket-navigate',
|
||||
name: 'Remarketing Navigation',
|
||||
description: 'Verify user navigated to remarketing tools',
|
||||
pillarId: 'remarket',
|
||||
actionType: 'navigate',
|
||||
weight: 0.8,
|
||||
verifier: async (task, context) => {
|
||||
return this.verifyNavigation(task, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify navigation-based tasks
|
||||
*/
|
||||
private async verifyNavigation(
|
||||
task: TodayTask,
|
||||
context?: VerificationContext
|
||||
): Promise<VerificationResult> {
|
||||
const evidence: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
const suggestions: string[] = [];
|
||||
let confidence = 0;
|
||||
|
||||
try {
|
||||
// Check if user is currently on the target page
|
||||
if (typeof window !== 'undefined' && task.actionUrl) {
|
||||
const currentPath = window.location.pathname;
|
||||
const targetPath = task.actionUrl;
|
||||
|
||||
if (currentPath === targetPath) {
|
||||
evidence.push(`User is currently on target page: ${targetPath}`);
|
||||
confidence += 0.4;
|
||||
} else {
|
||||
warnings.push(`User is not on target page. Current: ${currentPath}, Expected: ${targetPath}`);
|
||||
suggestions.push('Navigate to the correct page to complete this task');
|
||||
}
|
||||
}
|
||||
|
||||
// Check user activity (if available)
|
||||
if (context?.userActivity) {
|
||||
const activity = context.userActivity;
|
||||
const taskStartTime = task.startedAt?.getTime() || 0;
|
||||
const recentActivity = Object.entries(activity)
|
||||
.filter(([_, timestamp]) => typeof timestamp === 'number' && timestamp > taskStartTime);
|
||||
|
||||
if (recentActivity.length > 0) {
|
||||
evidence.push(`User activity detected after task start: ${recentActivity.length} actions`);
|
||||
confidence += 0.3;
|
||||
} else {
|
||||
warnings.push('No user activity detected after task start');
|
||||
}
|
||||
}
|
||||
|
||||
// Check platform data (if available)
|
||||
if (context?.platformData) {
|
||||
const platformData = context.platformData;
|
||||
if (platformData.lastActivity && platformData.lastActivity > (task.startedAt?.getTime() || 0)) {
|
||||
evidence.push('Platform activity detected after task start');
|
||||
confidence += 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
// Time-based verification
|
||||
if (task.startedAt && task.completedAt) {
|
||||
const timeSpent = task.completedAt.getTime() - task.startedAt.getTime();
|
||||
const estimatedTime = task.estimatedTime * 60 * 1000; // Convert to milliseconds
|
||||
|
||||
if (timeSpent >= estimatedTime * 0.5) { // At least 50% of estimated time
|
||||
evidence.push(`Task took ${Math.round(timeSpent / 60000)} minutes (estimated: ${task.estimatedTime} minutes)`);
|
||||
confidence += 0.2;
|
||||
} else {
|
||||
warnings.push(`Task completed too quickly (${Math.round(timeSpent / 60000)} minutes vs ${task.estimatedTime} estimated)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Cap confidence at 1.0
|
||||
confidence = Math.min(confidence, 1.0);
|
||||
|
||||
return {
|
||||
isCompleted: confidence >= 0.6, // Threshold for completion
|
||||
confidence,
|
||||
evidence,
|
||||
warnings,
|
||||
suggestions
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
isCompleted: false,
|
||||
confidence: 0,
|
||||
evidence: [],
|
||||
warnings: [`Navigation verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`],
|
||||
suggestions: ['Try navigating to the target page again']
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic verification fallback
|
||||
*/
|
||||
private async basicVerification(
|
||||
task: TodayTask,
|
||||
context?: VerificationContext
|
||||
): Promise<VerificationResult> {
|
||||
const evidence: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
const suggestions: string[] = [];
|
||||
let confidence = 0;
|
||||
|
||||
// Check if task has completion timestamp
|
||||
if (task.completedAt) {
|
||||
evidence.push('Task has completion timestamp');
|
||||
confidence += 0.5;
|
||||
} else {
|
||||
warnings.push('No completion timestamp found');
|
||||
}
|
||||
|
||||
// Check if task was started
|
||||
if (task.startedAt) {
|
||||
evidence.push('Task was started');
|
||||
confidence += 0.3;
|
||||
} else {
|
||||
warnings.push('No start timestamp found');
|
||||
}
|
||||
|
||||
// Check time spent
|
||||
if (task.startedAt && task.completedAt) {
|
||||
const timeSpent = task.completedAt.getTime() - task.startedAt.getTime();
|
||||
if (timeSpent > 0) {
|
||||
evidence.push(`Task took ${Math.round(timeSpent / 60000)} minutes`);
|
||||
confidence += 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isCompleted: confidence >= 0.5,
|
||||
confidence,
|
||||
evidence,
|
||||
warnings,
|
||||
suggestions: suggestions.length > 0 ? suggestions : ['Complete the task to verify completion']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Store verification result in history
|
||||
*/
|
||||
private storeVerificationResult(taskId: string, result: VerificationResult): void {
|
||||
const history = this.verificationHistory.get(taskId) || [];
|
||||
history.push(result);
|
||||
|
||||
// Keep only last 10 verification results
|
||||
if (history.length > 10) {
|
||||
history.shift();
|
||||
}
|
||||
|
||||
this.verificationHistory.set(taskId, history);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get verification statistics
|
||||
*/
|
||||
getVerificationStats(): {
|
||||
totalVerifications: number;
|
||||
averageConfidence: number;
|
||||
completionRate: number;
|
||||
mostCommonWarnings: string[];
|
||||
} {
|
||||
const allResults = Array.from(this.verificationHistory.values()).flat();
|
||||
|
||||
if (allResults.length === 0) {
|
||||
return {
|
||||
totalVerifications: 0,
|
||||
averageConfidence: 0,
|
||||
completionRate: 0,
|
||||
mostCommonWarnings: []
|
||||
};
|
||||
}
|
||||
|
||||
const totalVerifications = allResults.length;
|
||||
const averageConfidence = allResults.reduce((sum, result) => sum + result.confidence, 0) / totalVerifications;
|
||||
const completionRate = allResults.filter(result => result.isCompleted).length / totalVerifications;
|
||||
|
||||
// Count warning frequency
|
||||
const warningCounts = new Map<string, number>();
|
||||
allResults.forEach(result => {
|
||||
result.warnings.forEach(warning => {
|
||||
warningCounts.set(warning, (warningCounts.get(warning) || 0) + 1);
|
||||
});
|
||||
});
|
||||
|
||||
const mostCommonWarnings = Array.from(warningCounts.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 5)
|
||||
.map(([warning]) => warning);
|
||||
|
||||
return {
|
||||
totalVerifications,
|
||||
averageConfidence,
|
||||
completionRate,
|
||||
mostCommonWarnings
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear verification history
|
||||
*/
|
||||
clearVerificationHistory(): void {
|
||||
this.verificationHistory.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Export verification data
|
||||
*/
|
||||
exportVerificationData(): {
|
||||
rules: VerificationRule[];
|
||||
history: Record<string, VerificationResult[]>;
|
||||
stats: {
|
||||
totalVerifications: number;
|
||||
averageConfidence: number;
|
||||
completionRate: number;
|
||||
mostCommonWarnings: string[];
|
||||
};
|
||||
} {
|
||||
return {
|
||||
rules: this.getVerificationRules(),
|
||||
history: Object.fromEntries(this.verificationHistory),
|
||||
stats: this.getVerificationStats()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const taskCompletionVerifier = new TaskCompletionVerifier();
|
||||
export default TaskCompletionVerifier;
|
||||
433
frontend/src/services/TaskDependencyManager.ts
Normal file
433
frontend/src/services/TaskDependencyManager.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
import {
|
||||
TodayTask,
|
||||
DailyWorkflow,
|
||||
WorkflowError
|
||||
} from '../types/workflow';
|
||||
|
||||
interface DependencyGraph {
|
||||
[taskId: string]: {
|
||||
dependencies: string[];
|
||||
dependents: string[];
|
||||
status: 'ready' | 'blocked' | 'completed' | 'skipped';
|
||||
};
|
||||
}
|
||||
|
||||
interface DependencyValidationResult {
|
||||
isValid: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
readyTasks: string[];
|
||||
blockedTasks: string[];
|
||||
}
|
||||
|
||||
class TaskDependencyManager {
|
||||
private dependencyGraph: DependencyGraph = {};
|
||||
|
||||
/**
|
||||
* Build dependency graph from workflow tasks
|
||||
*/
|
||||
buildDependencyGraph(workflow: DailyWorkflow): DependencyGraph {
|
||||
this.dependencyGraph = {};
|
||||
|
||||
// Initialize all tasks in the graph
|
||||
workflow.tasks.forEach(task => {
|
||||
this.dependencyGraph[task.id] = {
|
||||
dependencies: task.dependencies || [],
|
||||
dependents: [],
|
||||
status: this.getTaskStatus(task, workflow)
|
||||
};
|
||||
});
|
||||
|
||||
// Build dependent relationships
|
||||
Object.keys(this.dependencyGraph).forEach(taskId => {
|
||||
const task = this.dependencyGraph[taskId];
|
||||
task.dependencies.forEach(depId => {
|
||||
if (this.dependencyGraph[depId]) {
|
||||
this.dependencyGraph[depId].dependents.push(taskId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return this.dependencyGraph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate dependency graph for issues
|
||||
*/
|
||||
validateDependencyGraph(workflow: DailyWorkflow): DependencyValidationResult {
|
||||
const result: DependencyValidationResult = {
|
||||
isValid: true,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
readyTasks: [],
|
||||
blockedTasks: []
|
||||
};
|
||||
|
||||
this.buildDependencyGraph(workflow);
|
||||
|
||||
// Check for circular dependencies
|
||||
const circularDeps = this.detectCircularDependencies();
|
||||
if (circularDeps.length > 0) {
|
||||
result.isValid = false;
|
||||
result.errors.push(`Circular dependencies detected: ${circularDeps.join(', ')}`);
|
||||
}
|
||||
|
||||
// Check for missing dependencies
|
||||
const missingDeps = this.detectMissingDependencies(workflow);
|
||||
if (missingDeps.length > 0) {
|
||||
result.isValid = false;
|
||||
result.errors.push(`Missing dependencies: ${missingDeps.join(', ')}`);
|
||||
}
|
||||
|
||||
// Check for orphaned tasks (tasks with no dependencies or dependents)
|
||||
const orphanedTasks = this.detectOrphanedTasks();
|
||||
if (orphanedTasks.length > 0) {
|
||||
result.warnings.push(`Orphaned tasks detected: ${orphanedTasks.join(', ')}`);
|
||||
}
|
||||
|
||||
// Categorize tasks by readiness
|
||||
Object.keys(this.dependencyGraph).forEach(taskId => {
|
||||
const task = this.dependencyGraph[taskId];
|
||||
if (task.status === 'ready') {
|
||||
result.readyTasks.push(taskId);
|
||||
} else if (task.status === 'blocked') {
|
||||
result.blockedTasks.push(taskId);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tasks that are ready to be executed
|
||||
*/
|
||||
getReadyTasks(workflow: DailyWorkflow): TodayTask[] {
|
||||
this.buildDependencyGraph(workflow);
|
||||
|
||||
return workflow.tasks.filter(task => {
|
||||
const graphTask = this.dependencyGraph[task.id];
|
||||
return graphTask && graphTask.status === 'ready' && task.status === 'pending';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tasks that are blocked by dependencies
|
||||
*/
|
||||
getBlockedTasks(workflow: DailyWorkflow): TodayTask[] {
|
||||
this.buildDependencyGraph(workflow);
|
||||
|
||||
return workflow.tasks.filter(task => {
|
||||
const graphTask = this.dependencyGraph[task.id];
|
||||
return graphTask && graphTask.status === 'blocked';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tasks that depend on a specific task
|
||||
*/
|
||||
getDependentTasks(taskId: string): string[] {
|
||||
const task = this.dependencyGraph[taskId];
|
||||
return task ? [...task.dependents] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tasks that a specific task depends on
|
||||
*/
|
||||
getDependencyTasks(taskId: string): string[] {
|
||||
const task = this.dependencyGraph[taskId];
|
||||
return task ? [...task.dependencies] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a task can be executed (all dependencies satisfied)
|
||||
*/
|
||||
canExecuteTask(taskId: string, workflow: DailyWorkflow): boolean {
|
||||
this.buildDependencyGraph(workflow);
|
||||
|
||||
const task = this.dependencyGraph[taskId];
|
||||
if (!task) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return task.status === 'ready';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the optimal execution order for tasks
|
||||
*/
|
||||
getOptimalExecutionOrder(workflow: DailyWorkflow): TodayTask[] {
|
||||
this.buildDependencyGraph(workflow);
|
||||
|
||||
const visited = new Set<string>();
|
||||
const visiting = new Set<string>();
|
||||
const executionOrder: TodayTask[] = [];
|
||||
|
||||
const visit = (taskId: string) => {
|
||||
if (visiting.has(taskId)) {
|
||||
throw new WorkflowError({
|
||||
code: 'CIRCULAR_DEPENDENCY',
|
||||
message: `Circular dependency detected involving task ${taskId}`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
if (visited.has(taskId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visiting.add(taskId);
|
||||
|
||||
const task = this.dependencyGraph[taskId];
|
||||
if (task) {
|
||||
// Visit dependencies first
|
||||
task.dependencies.forEach(depId => {
|
||||
visit(depId);
|
||||
});
|
||||
}
|
||||
|
||||
visiting.delete(taskId);
|
||||
visited.add(taskId);
|
||||
|
||||
// Add task to execution order
|
||||
const workflowTask = workflow.tasks.find(t => t.id === taskId);
|
||||
if (workflowTask) {
|
||||
executionOrder.push(workflowTask);
|
||||
}
|
||||
};
|
||||
|
||||
// Visit all tasks
|
||||
workflow.tasks.forEach(task => {
|
||||
if (!visited.has(task.id)) {
|
||||
visit(task.id);
|
||||
}
|
||||
});
|
||||
|
||||
return executionOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update task status in dependency graph
|
||||
*/
|
||||
updateTaskStatus(taskId: string, status: 'completed' | 'skipped' | 'in_progress'): void {
|
||||
if (this.dependencyGraph[taskId]) {
|
||||
// Update status of the task
|
||||
this.dependencyGraph[taskId].status = status === 'in_progress' ? 'ready' : status;
|
||||
|
||||
// Update status of dependent tasks
|
||||
this.updateDependentTasksStatus(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dependency chain for a task (all tasks that must be completed first)
|
||||
*/
|
||||
getDependencyChain(taskId: string): string[] {
|
||||
const chain: string[] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
const buildChain = (currentTaskId: string) => {
|
||||
if (visited.has(currentTaskId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visited.add(currentTaskId);
|
||||
const task = this.dependencyGraph[currentTaskId];
|
||||
|
||||
if (task) {
|
||||
task.dependencies.forEach(depId => {
|
||||
buildChain(depId);
|
||||
if (!chain.includes(depId)) {
|
||||
chain.push(depId);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
buildChain(taskId);
|
||||
return chain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get impact of completing a task (what tasks become available)
|
||||
*/
|
||||
getCompletionImpact(taskId: string): string[] {
|
||||
const impact: string[] = [];
|
||||
|
||||
const task = this.dependencyGraph[taskId];
|
||||
if (task) {
|
||||
task.dependents.forEach(dependentId => {
|
||||
const dependent = this.dependencyGraph[dependentId];
|
||||
if (dependent && dependent.status === 'blocked') {
|
||||
// Check if all dependencies are now satisfied
|
||||
const allDepsSatisfied = dependent.dependencies.every(depId => {
|
||||
const depTask = this.dependencyGraph[depId];
|
||||
return depTask && (depTask.status === 'completed' || depTask.status === 'skipped');
|
||||
});
|
||||
|
||||
if (allDepsSatisfied) {
|
||||
impact.push(dependentId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return impact;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect circular dependencies in the graph
|
||||
*/
|
||||
private detectCircularDependencies(): string[] {
|
||||
const visited = new Set<string>();
|
||||
const visiting = new Set<string>();
|
||||
const circular: string[] = [];
|
||||
|
||||
const visit = (taskId: string, path: string[] = []) => {
|
||||
if (visiting.has(taskId)) {
|
||||
const cycleStart = path.indexOf(taskId);
|
||||
if (cycleStart !== -1) {
|
||||
circular.push(...path.slice(cycleStart), taskId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (visited.has(taskId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visiting.add(taskId);
|
||||
const task = this.dependencyGraph[taskId];
|
||||
|
||||
if (task) {
|
||||
task.dependencies.forEach(depId => {
|
||||
visit(depId, [...path, taskId]);
|
||||
});
|
||||
}
|
||||
|
||||
visiting.delete(taskId);
|
||||
visited.add(taskId);
|
||||
};
|
||||
|
||||
Object.keys(this.dependencyGraph).forEach(taskId => {
|
||||
if (!visited.has(taskId)) {
|
||||
visit(taskId);
|
||||
}
|
||||
});
|
||||
|
||||
return [...new Set(circular)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect missing dependencies
|
||||
*/
|
||||
private detectMissingDependencies(workflow: DailyWorkflow): string[] {
|
||||
const missing: string[] = [];
|
||||
const taskIds = new Set(workflow.tasks.map(t => t.id));
|
||||
|
||||
Object.keys(this.dependencyGraph).forEach(taskId => {
|
||||
const task = this.dependencyGraph[taskId];
|
||||
task.dependencies.forEach(depId => {
|
||||
if (!taskIds.has(depId)) {
|
||||
missing.push(`${taskId} -> ${depId}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect orphaned tasks
|
||||
*/
|
||||
private detectOrphanedTasks(): string[] {
|
||||
const orphaned: string[] = [];
|
||||
|
||||
Object.keys(this.dependencyGraph).forEach(taskId => {
|
||||
const task = this.dependencyGraph[taskId];
|
||||
if (task.dependencies.length === 0 && task.dependents.length === 0) {
|
||||
orphaned.push(taskId);
|
||||
}
|
||||
});
|
||||
|
||||
return orphaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update dependent tasks status when a dependency is completed
|
||||
*/
|
||||
private updateDependentTasksStatus(completedTaskId: string): void {
|
||||
const task = this.dependencyGraph[completedTaskId];
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
|
||||
task.dependents.forEach(dependentId => {
|
||||
const dependent = this.dependencyGraph[dependentId];
|
||||
if (dependent && dependent.status === 'blocked') {
|
||||
// Check if all dependencies are now satisfied
|
||||
const allDepsSatisfied = dependent.dependencies.every(depId => {
|
||||
const depTask = this.dependencyGraph[depId];
|
||||
return depTask && (depTask.status === 'completed' || depTask.status === 'skipped');
|
||||
});
|
||||
|
||||
if (allDepsSatisfied) {
|
||||
dependent.status = 'ready';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get task status based on dependencies
|
||||
*/
|
||||
private getTaskStatus(task: TodayTask, workflow: DailyWorkflow): 'ready' | 'blocked' | 'completed' | 'skipped' {
|
||||
if (task.status === 'completed' || task.status === 'skipped') {
|
||||
return task.status;
|
||||
}
|
||||
|
||||
if (!task.dependencies || task.dependencies.length === 0) {
|
||||
return 'ready';
|
||||
}
|
||||
|
||||
// Check if all dependencies are satisfied
|
||||
const allDepsSatisfied = task.dependencies.every(depId => {
|
||||
const depTask = workflow.tasks.find(t => t.id === depId);
|
||||
return depTask && (depTask.status === 'completed' || depTask.status === 'skipped');
|
||||
});
|
||||
|
||||
return allDepsSatisfied ? 'ready' : 'blocked';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dependency graph visualization data
|
||||
*/
|
||||
getDependencyGraphData(): {
|
||||
nodes: Array<{ id: string; label: string; status: string }>;
|
||||
edges: Array<{ from: string; to: string; type: string }>;
|
||||
} {
|
||||
const nodes = Object.keys(this.dependencyGraph).map(taskId => ({
|
||||
id: taskId,
|
||||
label: taskId,
|
||||
status: this.dependencyGraph[taskId].status
|
||||
}));
|
||||
|
||||
const edges: Array<{ from: string; to: string; type: string }> = [];
|
||||
Object.keys(this.dependencyGraph).forEach(taskId => {
|
||||
const task = this.dependencyGraph[taskId];
|
||||
task.dependencies.forEach(depId => {
|
||||
edges.push({
|
||||
from: depId,
|
||||
to: taskId,
|
||||
type: 'dependency'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return { nodes, edges };
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const taskDependencyManager = new TaskDependencyManager();
|
||||
export default TaskDependencyManager;
|
||||
469
frontend/src/services/TaskNavigationService.ts
Normal file
469
frontend/src/services/TaskNavigationService.ts
Normal file
@@ -0,0 +1,469 @@
|
||||
import {
|
||||
TodayTask,
|
||||
DailyWorkflow,
|
||||
NavigationState,
|
||||
WorkflowError
|
||||
} from '../types/workflow';
|
||||
|
||||
interface NavigationConfig {
|
||||
autoNavigate: boolean;
|
||||
delayBeforeNavigation: number; // milliseconds
|
||||
showNavigationConfirmation: boolean;
|
||||
enableBackNavigation: boolean;
|
||||
persistNavigationState: boolean;
|
||||
}
|
||||
|
||||
interface NavigationEvent {
|
||||
type: 'task_started' | 'task_completed' | 'task_skipped' | 'navigation_requested';
|
||||
taskId: string;
|
||||
workflowId: string;
|
||||
timestamp: Date;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
class TaskNavigationService {
|
||||
private config: NavigationConfig;
|
||||
private navigationHistory: NavigationEvent[] = [];
|
||||
private currentNavigationState: NavigationState | null = null;
|
||||
private navigationListeners: Array<(event: NavigationEvent) => void> = [];
|
||||
|
||||
constructor(config: NavigationConfig = {
|
||||
autoNavigate: true,
|
||||
delayBeforeNavigation: 2000,
|
||||
showNavigationConfirmation: false,
|
||||
enableBackNavigation: true,
|
||||
persistNavigationState: true
|
||||
}) {
|
||||
this.config = config;
|
||||
this.loadNavigationHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a specific task
|
||||
*/
|
||||
async navigateToTask(
|
||||
task: TodayTask,
|
||||
workflow: DailyWorkflow,
|
||||
options: {
|
||||
skipConfirmation?: boolean;
|
||||
trackNavigation?: boolean;
|
||||
} = {}
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// Validate task and workflow
|
||||
if (!this.validateTaskForNavigation(task, workflow)) {
|
||||
throw new WorkflowError({
|
||||
code: 'INVALID_NAVIGATION_TARGET',
|
||||
message: `Cannot navigate to task ${task.id}`,
|
||||
timestamp: new Date(),
|
||||
recoverable: true,
|
||||
suggestedAction: 'Check task dependencies and status'
|
||||
});
|
||||
}
|
||||
|
||||
// Show confirmation if required
|
||||
if (this.config.showNavigationConfirmation && !options.skipConfirmation) {
|
||||
const confirmed = await this.showNavigationConfirmation(task);
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute navigation based on action type
|
||||
const navigationSuccess = await this.executeNavigation(task);
|
||||
|
||||
if (navigationSuccess) {
|
||||
// Update navigation state
|
||||
this.updateNavigationState(task, workflow);
|
||||
|
||||
// Track navigation event
|
||||
if (options.trackNavigation !== false) {
|
||||
this.trackNavigationEvent({
|
||||
type: 'navigation_requested',
|
||||
taskId: task.id,
|
||||
workflowId: workflow.id,
|
||||
timestamp: new Date(),
|
||||
metadata: { actionType: task.actionType, actionUrl: task.actionUrl }
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-navigate to next task if enabled
|
||||
if (this.config.autoNavigate && task.status === 'completed') {
|
||||
setTimeout(() => {
|
||||
this.autoNavigateToNextTask(workflow);
|
||||
}, this.config.delayBeforeNavigation);
|
||||
}
|
||||
}
|
||||
|
||||
return navigationSuccess;
|
||||
} catch (error) {
|
||||
console.error('Navigation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-navigate to the next available task
|
||||
*/
|
||||
async autoNavigateToNextTask(workflow: DailyWorkflow): Promise<TodayTask | null> {
|
||||
try {
|
||||
const nextTask = this.getNextAvailableTask(workflow);
|
||||
|
||||
if (nextTask) {
|
||||
await this.navigateToTask(nextTask, workflow, {
|
||||
skipConfirmation: true,
|
||||
trackNavigation: true
|
||||
});
|
||||
return nextTask;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Auto-navigation failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate back to previous task
|
||||
*/
|
||||
async navigateBack(workflow: DailyWorkflow): Promise<TodayTask | null> {
|
||||
if (!this.config.enableBackNavigation) {
|
||||
throw new WorkflowError({
|
||||
code: 'BACK_NAVIGATION_DISABLED',
|
||||
message: 'Back navigation is disabled',
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const previousTask = this.getPreviousTask(workflow);
|
||||
|
||||
if (previousTask) {
|
||||
await this.navigateToTask(previousTask, workflow, {
|
||||
skipConfirmation: true,
|
||||
trackNavigation: true
|
||||
});
|
||||
return previousTask;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Back navigation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next available task in the workflow
|
||||
*/
|
||||
getNextAvailableTask(workflow: DailyWorkflow): TodayTask | null {
|
||||
const currentIndex = workflow.currentTaskIndex;
|
||||
const remainingTasks = workflow.tasks.slice(currentIndex + 1);
|
||||
|
||||
// Find next task that's not completed and has dependencies satisfied
|
||||
for (const task of remainingTasks) {
|
||||
if (task.status === 'pending' && this.areDependenciesSatisfied(task, workflow)) {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the previous task in the workflow
|
||||
*/
|
||||
getPreviousTask(workflow: DailyWorkflow): TodayTask | null {
|
||||
const currentIndex = workflow.currentTaskIndex;
|
||||
|
||||
if (currentIndex > 0) {
|
||||
return workflow.tasks[currentIndex - 1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if task dependencies are satisfied
|
||||
*/
|
||||
areDependenciesSatisfied(task: TodayTask, workflow: DailyWorkflow): boolean {
|
||||
if (!task.dependencies || task.dependencies.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return task.dependencies.every(depId => {
|
||||
const depTask = workflow.tasks.find(t => t.id === depId);
|
||||
return depTask && depTask.status === 'completed';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the actual navigation based on task action type
|
||||
*/
|
||||
private async executeNavigation(task: TodayTask): Promise<boolean> {
|
||||
try {
|
||||
switch (task.actionType) {
|
||||
case 'navigate':
|
||||
return await this.navigateToInternalPage(task);
|
||||
case 'modal':
|
||||
return await this.openModal(task);
|
||||
case 'external':
|
||||
return await this.navigateToExternalUrl(task);
|
||||
default:
|
||||
throw new WorkflowError({
|
||||
code: 'UNKNOWN_ACTION_TYPE',
|
||||
message: `Unknown action type: ${task.actionType}`,
|
||||
timestamp: new Date(),
|
||||
recoverable: true,
|
||||
suggestedAction: 'Check task configuration'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Navigation execution failed for task ${task.id}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to internal ALwrity page
|
||||
*/
|
||||
private async navigateToInternalPage(task: TodayTask): Promise<boolean> {
|
||||
if (!task.actionUrl) {
|
||||
throw new WorkflowError({
|
||||
code: 'MISSING_ACTION_URL',
|
||||
message: `Task ${task.id} is missing action URL`,
|
||||
timestamp: new Date(),
|
||||
recoverable: true,
|
||||
suggestedAction: 'Configure action URL for the task'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Use React Router navigation
|
||||
if (typeof window !== 'undefined' && window.history) {
|
||||
window.history.pushState(null, '', task.actionUrl);
|
||||
|
||||
// Dispatch custom event for React Router to handle
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Internal navigation failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open modal for task
|
||||
*/
|
||||
private async openModal(task: TodayTask): Promise<boolean> {
|
||||
try {
|
||||
// Dispatch custom event to open modal
|
||||
const modalEvent = new CustomEvent('openTaskModal', {
|
||||
detail: { task }
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(modalEvent);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Modal opening failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to external URL
|
||||
*/
|
||||
private async navigateToExternalUrl(task: TodayTask): Promise<boolean> {
|
||||
if (!task.actionUrl) {
|
||||
throw new WorkflowError({
|
||||
code: 'MISSING_ACTION_URL',
|
||||
message: `Task ${task.id} is missing external URL`,
|
||||
timestamp: new Date(),
|
||||
recoverable: true,
|
||||
suggestedAction: 'Configure external URL for the task'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.open(task.actionUrl, '_blank', 'noopener,noreferrer');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('External navigation failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if task can be navigated to
|
||||
*/
|
||||
private validateTaskForNavigation(task: TodayTask, workflow: DailyWorkflow): boolean {
|
||||
// Check if task exists in workflow
|
||||
const workflowTask = workflow.tasks.find(t => t.id === task.id);
|
||||
if (!workflowTask) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if task is enabled
|
||||
if (!task.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check dependencies
|
||||
if (!this.areDependenciesSatisfied(task, workflow)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show navigation confirmation dialog
|
||||
*/
|
||||
private async showNavigationConfirmation(task: TodayTask): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
// In a real implementation, this would show a confirmation dialog
|
||||
// For now, we'll use a simple confirm dialog
|
||||
const confirmed = window.confirm(
|
||||
`Navigate to: ${task.title}\n\n${task.description}\n\nContinue?`
|
||||
);
|
||||
resolve(confirmed);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update navigation state
|
||||
*/
|
||||
private updateNavigationState(task: TodayTask, workflow: DailyWorkflow): void {
|
||||
const currentIndex = workflow.tasks.findIndex(t => t.id === task.id);
|
||||
const previousTask = currentIndex > 0 ? workflow.tasks[currentIndex - 1] : null;
|
||||
const nextTask = currentIndex < workflow.tasks.length - 1 ? workflow.tasks[currentIndex + 1] : null;
|
||||
|
||||
this.currentNavigationState = {
|
||||
currentTask: task,
|
||||
previousTask,
|
||||
nextTask,
|
||||
canGoBack: currentIndex > 0,
|
||||
canGoForward: currentIndex < workflow.tasks.length - 1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Track navigation event
|
||||
*/
|
||||
private trackNavigationEvent(event: NavigationEvent): void {
|
||||
this.navigationHistory.push(event);
|
||||
|
||||
// Notify listeners
|
||||
this.navigationListeners.forEach(listener => {
|
||||
try {
|
||||
listener(event);
|
||||
} catch (error) {
|
||||
console.error('Navigation listener error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Persist navigation history
|
||||
if (this.config.persistNavigationState) {
|
||||
this.persistNavigationHistory();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add navigation event listener
|
||||
*/
|
||||
addNavigationListener(listener: (event: NavigationEvent) => void): void {
|
||||
this.navigationListeners.push(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove navigation event listener
|
||||
*/
|
||||
removeNavigationListener(listener: (event: NavigationEvent) => void): void {
|
||||
const index = this.navigationListeners.indexOf(listener);
|
||||
if (index > -1) {
|
||||
this.navigationListeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current navigation state
|
||||
*/
|
||||
getCurrentNavigationState(): NavigationState | null {
|
||||
return this.currentNavigationState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get navigation history
|
||||
*/
|
||||
getNavigationHistory(): NavigationEvent[] {
|
||||
return [...this.navigationHistory];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear navigation history
|
||||
*/
|
||||
clearNavigationHistory(): void {
|
||||
this.navigationHistory = [];
|
||||
this.persistNavigationHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist navigation history to localStorage
|
||||
*/
|
||||
private persistNavigationHistory(): void {
|
||||
try {
|
||||
localStorage.setItem('task-navigation-history', JSON.stringify(this.navigationHistory));
|
||||
} catch (error) {
|
||||
console.warn('Failed to persist navigation history:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load navigation history from localStorage
|
||||
*/
|
||||
private loadNavigationHistory(): void {
|
||||
try {
|
||||
const stored = localStorage.getItem('task-navigation-history');
|
||||
if (stored) {
|
||||
this.navigationHistory = JSON.parse(stored).map((event: any) => ({
|
||||
...event,
|
||||
timestamp: new Date(event.timestamp)
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load navigation history:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update navigation configuration
|
||||
*/
|
||||
updateConfig(newConfig: Partial<NavigationConfig>): void {
|
||||
this.config = { ...this.config, ...newConfig };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current configuration
|
||||
*/
|
||||
getConfig(): NavigationConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const taskNavigationService = new TaskNavigationService();
|
||||
export default TaskNavigationService;
|
||||
611
frontend/src/services/TaskWorkflowOrchestrator.ts
Normal file
611
frontend/src/services/TaskWorkflowOrchestrator.ts
Normal file
@@ -0,0 +1,611 @@
|
||||
import {
|
||||
TodayTask,
|
||||
DailyWorkflow,
|
||||
WorkflowProgress,
|
||||
TaskCompletionData,
|
||||
TaskGenerationContext,
|
||||
WorkflowOrchestratorConfig,
|
||||
NavigationState,
|
||||
WorkflowError
|
||||
} from '../types/workflow';
|
||||
import { taskNavigationService } from './TaskNavigationService';
|
||||
import { taskDependencyManager } from './TaskDependencyManager';
|
||||
import { taskCompletionVerifier } from './TaskCompletionVerifier';
|
||||
|
||||
class TaskWorkflowOrchestrator {
|
||||
private workflows: Map<string, DailyWorkflow> = new Map();
|
||||
private config: WorkflowOrchestratorConfig;
|
||||
|
||||
constructor(config: WorkflowOrchestratorConfig = {
|
||||
autoNavigate: true,
|
||||
showProgress: true,
|
||||
enableNotifications: true,
|
||||
persistProgress: true,
|
||||
allowTaskSkipping: true
|
||||
}) {
|
||||
this.config = config;
|
||||
this.loadPersistedWorkflows();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new daily workflow for a user
|
||||
*/
|
||||
async generateDailyWorkflow(
|
||||
userId: string,
|
||||
date: string = new Date().toISOString().split('T')[0],
|
||||
context?: TaskGenerationContext
|
||||
): Promise<DailyWorkflow> {
|
||||
try {
|
||||
// Check if workflow already exists for this date
|
||||
const existingWorkflow = this.getWorkflow(userId, date);
|
||||
if (existingWorkflow) {
|
||||
return existingWorkflow;
|
||||
}
|
||||
|
||||
// Generate tasks based on context or default configuration
|
||||
const tasks = await this.generateTasksForDate(userId, date, context);
|
||||
|
||||
// Create new workflow
|
||||
const workflow: DailyWorkflow = {
|
||||
id: `${userId}-${date}`,
|
||||
date,
|
||||
userId,
|
||||
tasks,
|
||||
currentTaskIndex: 0,
|
||||
completedTasks: 0,
|
||||
totalTasks: tasks.length,
|
||||
workflowStatus: 'not_started',
|
||||
totalEstimatedTime: tasks.reduce((sum, task) => sum + task.estimatedTime, 0),
|
||||
actualTimeSpent: 0
|
||||
};
|
||||
|
||||
// Save workflow
|
||||
this.workflows.set(workflow.id, workflow);
|
||||
this.persistWorkflow(workflow);
|
||||
|
||||
return workflow;
|
||||
} catch (error) {
|
||||
throw new WorkflowError({
|
||||
code: 'WORKFLOW_GENERATION_FAILED',
|
||||
message: `Failed to generate workflow for user ${userId} on ${date}`,
|
||||
timestamp: new Date(),
|
||||
recoverable: true,
|
||||
suggestedAction: 'Retry workflow generation'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workflow for a specific user and date
|
||||
*/
|
||||
getWorkflow(userId: string, date: string): DailyWorkflow | null {
|
||||
const workflowId = `${userId}-${date}`;
|
||||
return this.workflows.get(workflowId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a workflow
|
||||
*/
|
||||
async startWorkflow(workflowId: string): Promise<DailyWorkflow> {
|
||||
const workflow = this.workflows.get(workflowId);
|
||||
if (!workflow) {
|
||||
throw new WorkflowError({
|
||||
code: 'WORKFLOW_NOT_FOUND',
|
||||
message: `Workflow ${workflowId} not found`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
workflow.workflowStatus = 'in_progress';
|
||||
workflow.startedAt = new Date();
|
||||
|
||||
// Mark first task as in progress
|
||||
if (workflow.tasks.length > 0) {
|
||||
workflow.tasks[0].status = 'in_progress';
|
||||
workflow.tasks[0].startedAt = new Date();
|
||||
}
|
||||
|
||||
this.persistWorkflow(workflow);
|
||||
return workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a specific task
|
||||
*/
|
||||
async completeTask(
|
||||
workflowId: string,
|
||||
taskId: string,
|
||||
completionData?: Partial<TaskCompletionData>
|
||||
): Promise<WorkflowProgress> {
|
||||
const workflow = this.workflows.get(workflowId);
|
||||
if (!workflow) {
|
||||
throw new WorkflowError({
|
||||
code: 'WORKFLOW_NOT_FOUND',
|
||||
message: `Workflow ${workflowId} not found`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
const task = workflow.tasks.find(t => t.id === taskId);
|
||||
if (!task) {
|
||||
throw new WorkflowError({
|
||||
code: 'TASK_NOT_FOUND',
|
||||
message: `Task ${taskId} not found in workflow ${workflowId}`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
// Verify task completion
|
||||
await taskCompletionVerifier.verifyTaskCompletion(task, {
|
||||
userId: workflow.userId,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
// Mark task as completed
|
||||
task.status = 'completed';
|
||||
task.completedAt = new Date();
|
||||
|
||||
// Calculate time spent
|
||||
if (task.startedAt) {
|
||||
const timeSpent = Math.round((task.completedAt.getTime() - task.startedAt.getTime()) / (1000 * 60));
|
||||
workflow.actualTimeSpent += timeSpent;
|
||||
}
|
||||
|
||||
// Update dependency manager
|
||||
taskDependencyManager.updateTaskStatus(taskId, 'completed');
|
||||
|
||||
// Update workflow progress
|
||||
workflow.completedTasks++;
|
||||
|
||||
// Check if workflow is complete
|
||||
if (workflow.completedTasks === workflow.totalTasks) {
|
||||
workflow.workflowStatus = 'completed';
|
||||
workflow.completedAt = new Date();
|
||||
}
|
||||
|
||||
// Auto-navigate to next task if enabled
|
||||
if (this.config.autoNavigate) {
|
||||
const nextTask = taskDependencyManager.getReadyTasks(workflow)[0];
|
||||
if (nextTask) {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await taskNavigationService.navigateToTask(nextTask, workflow);
|
||||
} catch (error) {
|
||||
console.warn('Auto-navigation failed:', error);
|
||||
}
|
||||
}, 2000); // 2 second delay
|
||||
}
|
||||
}
|
||||
|
||||
this.persistWorkflow(workflow);
|
||||
return this.getWorkflowProgress(workflowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip a task
|
||||
*/
|
||||
async skipTask(workflowId: string, taskId: string): Promise<WorkflowProgress> {
|
||||
const workflow = this.workflows.get(workflowId);
|
||||
if (!workflow) {
|
||||
throw new WorkflowError({
|
||||
code: 'WORKFLOW_NOT_FOUND',
|
||||
message: `Workflow ${workflowId} not found`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
const task = workflow.tasks.find(t => t.id === taskId);
|
||||
if (!task) {
|
||||
throw new WorkflowError({
|
||||
code: 'TASK_NOT_FOUND',
|
||||
message: `Task ${taskId} not found in workflow ${workflowId}`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
task.status = 'skipped';
|
||||
workflow.completedTasks++;
|
||||
|
||||
this.persistWorkflow(workflow);
|
||||
return this.getWorkflowProgress(workflowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current workflow progress
|
||||
*/
|
||||
getWorkflowProgress(workflowId: string): WorkflowProgress {
|
||||
const workflow = this.workflows.get(workflowId);
|
||||
if (!workflow) {
|
||||
throw new WorkflowError({
|
||||
code: 'WORKFLOW_NOT_FOUND',
|
||||
message: `Workflow ${workflowId} not found`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
const currentTask = workflow.tasks[workflow.currentTaskIndex];
|
||||
const nextTask = workflow.tasks[workflow.currentTaskIndex + 1];
|
||||
const remainingTasks = workflow.tasks.slice(workflow.currentTaskIndex + 1);
|
||||
const estimatedTimeRemaining = remainingTasks.reduce((sum, task) => sum + task.estimatedTime, 0);
|
||||
|
||||
return {
|
||||
completedTasks: workflow.completedTasks,
|
||||
totalTasks: workflow.totalTasks,
|
||||
completionPercentage: Math.round((workflow.completedTasks / workflow.totalTasks) * 100),
|
||||
currentTask,
|
||||
nextTask,
|
||||
estimatedTimeRemaining,
|
||||
actualTimeSpent: workflow.actualTimeSpent
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get navigation state for current workflow
|
||||
*/
|
||||
getNavigationState(workflowId: string): NavigationState {
|
||||
const workflow = this.workflows.get(workflowId);
|
||||
if (!workflow) {
|
||||
throw new WorkflowError({
|
||||
code: 'WORKFLOW_NOT_FOUND',
|
||||
message: `Workflow ${workflowId} not found`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
const currentTask = workflow.tasks[workflow.currentTaskIndex];
|
||||
const previousTask = workflow.currentTaskIndex > 0 ? workflow.tasks[workflow.currentTaskIndex - 1] : null;
|
||||
const nextTask = workflow.currentTaskIndex < workflow.tasks.length - 1 ? workflow.tasks[workflow.currentTaskIndex + 1] : null;
|
||||
|
||||
return {
|
||||
currentTask,
|
||||
previousTask,
|
||||
nextTask,
|
||||
canGoBack: workflow.currentTaskIndex > 0,
|
||||
canGoForward: workflow.currentTaskIndex < workflow.tasks.length - 1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to next task in workflow
|
||||
*/
|
||||
async moveToNextTask(workflowId: string): Promise<TodayTask | null> {
|
||||
const workflow = this.workflows.get(workflowId);
|
||||
if (!workflow) {
|
||||
throw new WorkflowError({
|
||||
code: 'WORKFLOW_NOT_FOUND',
|
||||
message: `Workflow ${workflowId} not found`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
if (workflow.currentTaskIndex < workflow.tasks.length - 1) {
|
||||
workflow.currentTaskIndex++;
|
||||
const nextTask = workflow.tasks[workflow.currentTaskIndex];
|
||||
|
||||
// Mark next task as in progress
|
||||
if (nextTask.status === 'pending') {
|
||||
nextTask.status = 'in_progress';
|
||||
nextTask.startedAt = new Date();
|
||||
}
|
||||
|
||||
this.persistWorkflow(workflow);
|
||||
return nextTask;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate tasks for a specific date (enhanced with dependency management)
|
||||
*/
|
||||
private async generateTasksForDate(
|
||||
userId: string,
|
||||
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`,
|
||||
pillarId: 'plan',
|
||||
title: 'Review content strategy',
|
||||
description: 'Check and update your content strategy for the week',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
estimatedTime: 15,
|
||||
actionType: 'navigate',
|
||||
actionUrl: '/content-planning-dashboard',
|
||||
enabled: true,
|
||||
icon: 'Business',
|
||||
color: '#4CAF50'
|
||||
},
|
||||
{
|
||||
id: `${userId}-${date}-plan-2`,
|
||||
pillarId: 'plan',
|
||||
title: 'Update content calendar',
|
||||
description: 'Review and update your content calendar',
|
||||
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`],
|
||||
actionType: 'navigate',
|
||||
actionUrl: '/facebook-writer',
|
||||
enabled: true,
|
||||
icon: 'AutoAwesome',
|
||||
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`,
|
||||
pillarId: 'publish',
|
||||
title: 'Publish social media content',
|
||||
description: 'Publish your created content to social media',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
estimatedTime: 10,
|
||||
dependencies: [`${userId}-${date}-generate-1`],
|
||||
actionType: 'navigate',
|
||||
actionUrl: '/facebook-writer',
|
||||
enabled: true,
|
||||
icon: 'Publish',
|
||||
color: '#FF9800'
|
||||
},
|
||||
{
|
||||
id: `${userId}-${date}-publish-2`,
|
||||
pillarId: 'publish',
|
||||
title: 'Publish blog content',
|
||||
description: 'Publish blog posts to your website',
|
||||
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`],
|
||||
actionType: 'navigate',
|
||||
actionUrl: '/analytics-dashboard',
|
||||
enabled: true,
|
||||
icon: 'Analytics',
|
||||
color: '#9C27B0'
|
||||
},
|
||||
{
|
||||
id: `${userId}-${date}-engage-1`,
|
||||
pillarId: 'engage',
|
||||
title: 'Respond to comments',
|
||||
description: 'Engage with comments on your content',
|
||||
status: 'pending',
|
||||
priority: 'low',
|
||||
estimatedTime: 15,
|
||||
dependencies: [`${userId}-${date}-publish-1`],
|
||||
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`,
|
||||
pillarId: 'remarket',
|
||||
title: 'Launch Retargeting Campaign',
|
||||
description: 'Create and launch targeted remarketing campaigns',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
estimatedTime: 35,
|
||||
dependencies: [`${userId}-${date}-engage-2`],
|
||||
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'
|
||||
}
|
||||
];
|
||||
|
||||
// Validate dependencies and get optimal execution order
|
||||
const tempWorkflow: DailyWorkflow = {
|
||||
id: `${userId}-${date}`,
|
||||
date,
|
||||
userId,
|
||||
tasks: defaultTasks,
|
||||
currentTaskIndex: 0,
|
||||
completedTasks: 0,
|
||||
totalTasks: defaultTasks.length,
|
||||
workflowStatus: 'not_started',
|
||||
totalEstimatedTime: defaultTasks.reduce((sum, task) => sum + task.estimatedTime, 0),
|
||||
actualTimeSpent: 0
|
||||
};
|
||||
|
||||
// Validate dependency graph
|
||||
const validation = taskDependencyManager.validateDependencyGraph(tempWorkflow);
|
||||
if (!validation.isValid) {
|
||||
console.warn('Dependency validation failed:', validation.errors);
|
||||
// Return tasks without dependencies if validation fails
|
||||
return defaultTasks.map(task => ({ ...task, dependencies: [] }));
|
||||
}
|
||||
|
||||
// Get optimal execution order
|
||||
const orderedTasks = taskDependencyManager.getOptimalExecutionOrder(tempWorkflow);
|
||||
|
||||
return orderedTasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist workflow to localStorage
|
||||
*/
|
||||
private persistWorkflow(workflow: DailyWorkflow): void {
|
||||
if (this.config.persistProgress) {
|
||||
try {
|
||||
localStorage.setItem(`workflow-${workflow.id}`, JSON.stringify(workflow));
|
||||
} catch (error) {
|
||||
console.warn('Failed to persist workflow:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load persisted workflows from localStorage
|
||||
*/
|
||||
private loadPersistedWorkflows(): void {
|
||||
if (this.config.persistProgress) {
|
||||
try {
|
||||
const keys = Object.keys(localStorage).filter(key => key.startsWith('workflow-'));
|
||||
keys.forEach(key => {
|
||||
const workflowData = localStorage.getItem(key);
|
||||
if (workflowData) {
|
||||
try {
|
||||
const workflow = JSON.parse(workflowData) as DailyWorkflow;
|
||||
|
||||
// Ensure workflow has required properties
|
||||
if (!workflow.id || !workflow.date || !workflow.userId) {
|
||||
console.warn(`Invalid workflow data for key ${key}, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure tasks array exists and is valid
|
||||
if (!workflow.tasks || !Array.isArray(workflow.tasks)) {
|
||||
console.warn(`Invalid tasks array for workflow ${workflow.id}, initializing empty array`);
|
||||
workflow.tasks = [];
|
||||
}
|
||||
|
||||
// Convert date strings back to Date objects
|
||||
if (workflow.startedAt) workflow.startedAt = new Date(workflow.startedAt);
|
||||
if (workflow.completedAt) workflow.completedAt = new Date(workflow.completedAt);
|
||||
|
||||
// Process tasks with null checks
|
||||
workflow.tasks.forEach(task => {
|
||||
if (task && typeof task === 'object') {
|
||||
if (task.startedAt) task.startedAt = new Date(task.startedAt);
|
||||
if (task.completedAt) task.completedAt = new Date(task.completedAt);
|
||||
}
|
||||
});
|
||||
|
||||
this.workflows.set(workflow.id, workflow);
|
||||
} catch (parseError) {
|
||||
console.warn(`Failed to parse workflow data for key ${key}:`, parseError);
|
||||
// Remove corrupted data
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to load persisted workflows:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear completed workflows (cleanup)
|
||||
*/
|
||||
clearCompletedWorkflows(): void {
|
||||
const completedWorkflows = Array.from(this.workflows.values())
|
||||
.filter(workflow => workflow.workflowStatus === 'completed');
|
||||
|
||||
completedWorkflows.forEach(workflow => {
|
||||
this.workflows.delete(workflow.id);
|
||||
if (this.config.persistProgress) {
|
||||
localStorage.removeItem(`workflow-${workflow.id}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const taskWorkflowOrchestrator = new TaskWorkflowOrchestrator();
|
||||
export default TaskWorkflowOrchestrator;
|
||||
Reference in New Issue
Block a user