Add SEO fields to Tasks model, improve content generation response handling, and enhance progress bar animation
- Added primary_keyword, secondary_keywords, tags, and categories fields to Tasks model - Updated generate_content function to handle full JSON response with all SEO fields - Improved progress bar animation: smooth 1% increments every 300ms - Enhanced step detection for content generation vs clustering vs ideas - Fixed progress modal to show correct messages for each function type - Added comprehensive logging to Keywords and Tasks pages for AI functions - Fixed error handling to show meaningful error messages instead of generic failures
This commit is contained in:
@@ -44,6 +44,7 @@ export function useProgressModal(): UseProgressModalReturn {
|
||||
// Step mapping with user-friendly messages and percentages
|
||||
const getStepInfo = (stepName: string, message: string = '', allSteps: any[] = []): { percentage: number; friendlyMessage: string } => {
|
||||
const stepUpper = stepName?.toUpperCase() || '';
|
||||
const messageLower = message.toLowerCase();
|
||||
|
||||
// Extract values from message and step messages
|
||||
const extractNumber = (pattern: RegExp, text: string): string => {
|
||||
@@ -54,11 +55,13 @@ export function useProgressModal(): UseProgressModalReturn {
|
||||
// Check message first, then all step messages
|
||||
let keywordCount = extractNumber(/(\d+)\s+keyword/i, message);
|
||||
let clusterCount = extractNumber(/(\d+)\s+cluster/i, message);
|
||||
let taskCount = extractNumber(/(\d+)\s+task/i, message);
|
||||
let itemCount = extractNumber(/(\d+)\s+item/i, message);
|
||||
|
||||
// Also check for "Loaded X items" or "Created X clusters" patterns
|
||||
if (!keywordCount) {
|
||||
if (!keywordCount && !taskCount && !itemCount) {
|
||||
const loadedMatch = extractNumber(/loaded\s+(\d+)\s+items?/i, message);
|
||||
if (loadedMatch) keywordCount = loadedMatch;
|
||||
if (loadedMatch) itemCount = loadedMatch;
|
||||
}
|
||||
if (!clusterCount) {
|
||||
const createdMatch = extractNumber(/created\s+(\d+)\s+clusters?/i, message);
|
||||
@@ -66,50 +69,106 @@ export function useProgressModal(): UseProgressModalReturn {
|
||||
}
|
||||
|
||||
// Check all steps if not found in message
|
||||
if (!keywordCount || !clusterCount) {
|
||||
if (!keywordCount && !taskCount && !itemCount) {
|
||||
for (const step of allSteps) {
|
||||
const stepMsg = step.message || '';
|
||||
if (!keywordCount) {
|
||||
keywordCount = extractNumber(/(\d+)\s+keyword/i, stepMsg) || extractNumber(/loaded\s+(\d+)\s+items?/i, stepMsg);
|
||||
keywordCount = extractNumber(/(\d+)\s+keyword/i, stepMsg);
|
||||
}
|
||||
if (!taskCount) {
|
||||
taskCount = extractNumber(/(\d+)\s+task/i, stepMsg);
|
||||
}
|
||||
if (!itemCount) {
|
||||
itemCount = extractNumber(/loaded\s+(\d+)\s+items?/i, stepMsg);
|
||||
}
|
||||
if (!clusterCount) {
|
||||
clusterCount = extractNumber(/(\d+)\s+cluster/i, stepMsg) || extractNumber(/created\s+(\d+)\s+clusters?/i, stepMsg);
|
||||
}
|
||||
if (keywordCount && clusterCount) break;
|
||||
if ((keywordCount || taskCount || itemCount) && clusterCount) break;
|
||||
}
|
||||
}
|
||||
|
||||
const finalKeywordCount = keywordCount;
|
||||
const finalClusterCount = clusterCount;
|
||||
const finalTaskCount = taskCount || itemCount;
|
||||
|
||||
// Determine function type from message/title context
|
||||
const isContentGeneration = messageLower.includes('content') || messageLower.includes('generating content') || messageLower.includes('article');
|
||||
const isClustering = messageLower.includes('cluster') && !messageLower.includes('content');
|
||||
const isIdeas = messageLower.includes('idea');
|
||||
|
||||
// Map steps to percentages and user-friendly messages
|
||||
if (stepUpper.includes('INIT') || stepUpper.includes('INITIALIZ')) {
|
||||
return { percentage: 0, friendlyMessage: 'Getting started...' };
|
||||
}
|
||||
if (stepUpper.includes('PREP') || stepUpper.includes('PREPAR')) {
|
||||
const msg = finalKeywordCount ? `Preparing ${finalKeywordCount} keyword${finalKeywordCount !== '1' ? 's' : ''}...` : 'Preparing your keywords...';
|
||||
return { percentage: 16, friendlyMessage: msg };
|
||||
if (isContentGeneration) {
|
||||
const msg = finalTaskCount ? `Preparing ${finalTaskCount} task${finalTaskCount !== '1' ? 's' : ''}...` : 'Preparing content generation...';
|
||||
return { percentage: 10, friendlyMessage: msg };
|
||||
} else if (isClustering) {
|
||||
const msg = finalKeywordCount ? `Preparing ${finalKeywordCount} keyword${finalKeywordCount !== '1' ? 's' : ''}...` : 'Preparing your keywords...';
|
||||
return { percentage: 16, friendlyMessage: msg };
|
||||
} else if (isIdeas) {
|
||||
const msg = finalClusterCount ? `Preparing ${finalClusterCount} cluster${finalClusterCount !== '1' ? 's' : ''}...` : 'Preparing clusters...';
|
||||
return { percentage: 10, friendlyMessage: msg };
|
||||
}
|
||||
return { percentage: 10, friendlyMessage: 'Preparing...' };
|
||||
}
|
||||
if (stepUpper.includes('AI_CALL') || stepUpper.includes('CALLING')) {
|
||||
return { percentage: 50, friendlyMessage: 'Finding related keywords...' };
|
||||
if (isContentGeneration) {
|
||||
return { percentage: 50, friendlyMessage: 'Generating content with AI...' };
|
||||
} else if (isClustering) {
|
||||
return { percentage: 50, friendlyMessage: 'Finding related keywords...' };
|
||||
} else if (isIdeas) {
|
||||
return { percentage: 50, friendlyMessage: 'Generating ideas...' };
|
||||
}
|
||||
return { percentage: 50, friendlyMessage: 'Processing with AI...' };
|
||||
}
|
||||
if (stepUpper.includes('PARSE') || stepUpper.includes('PARSING')) {
|
||||
return { percentage: 70, friendlyMessage: 'Organizing results...' };
|
||||
if (isContentGeneration) {
|
||||
return { percentage: 70, friendlyMessage: 'Processing content...' };
|
||||
} else if (isClustering) {
|
||||
return { percentage: 70, friendlyMessage: 'Organizing results...' };
|
||||
} else if (isIdeas) {
|
||||
return { percentage: 70, friendlyMessage: 'Processing ideas...' };
|
||||
}
|
||||
return { percentage: 70, friendlyMessage: 'Processing results...' };
|
||||
}
|
||||
if (stepUpper.includes('SAVE') || stepUpper.includes('SAVING') || stepUpper.includes('CREAT')) {
|
||||
const msg = finalClusterCount ? `Saving ${finalClusterCount} cluster${finalClusterCount !== '1' ? 's' : ''}...` : 'Saving clusters...';
|
||||
return { percentage: 85, friendlyMessage: msg };
|
||||
if (stepUpper.includes('SAVE') || stepUpper.includes('SAVING') || (stepUpper.includes('CREAT') && !stepUpper.includes('CONTENT'))) {
|
||||
if (isContentGeneration) {
|
||||
const msg = finalTaskCount ? `Saving content for ${finalTaskCount} task${finalTaskCount !== '1' ? 's' : ''}...` : 'Saving content...';
|
||||
return { percentage: 85, friendlyMessage: msg };
|
||||
} else if (isClustering) {
|
||||
const msg = finalClusterCount ? `Saving ${finalClusterCount} cluster${finalClusterCount !== '1' ? 's' : ''}...` : 'Saving clusters...';
|
||||
return { percentage: 85, friendlyMessage: msg };
|
||||
} else if (isIdeas) {
|
||||
const msg = finalTaskCount ? `Saving ${finalTaskCount} idea${finalTaskCount !== '1' ? 's' : ''}...` : 'Saving ideas...';
|
||||
return { percentage: 85, friendlyMessage: msg };
|
||||
}
|
||||
return { percentage: 85, friendlyMessage: 'Saving results...' };
|
||||
}
|
||||
if (stepUpper.includes('DONE') || stepUpper.includes('COMPLETE')) {
|
||||
// Use extracted counts for completion message
|
||||
const finalMsg = finalKeywordCount && finalClusterCount
|
||||
? `Done! Created ${finalClusterCount} cluster${finalClusterCount !== '1' ? 's' : ''} from ${finalKeywordCount} keyword${finalKeywordCount !== '1' ? 's' : ''}`
|
||||
: finalKeywordCount
|
||||
? `Done! Processed ${finalKeywordCount} keyword${finalKeywordCount !== '1' ? 's' : ''}`
|
||||
: finalClusterCount
|
||||
? `Done! Created ${finalClusterCount} cluster${finalClusterCount !== '1' ? 's' : ''}`
|
||||
: 'Done! Clustering complete';
|
||||
return { percentage: 100, friendlyMessage: finalMsg };
|
||||
if (isContentGeneration) {
|
||||
const finalMsg = finalTaskCount
|
||||
? `Done! Generated content for ${finalTaskCount} task${finalTaskCount !== '1' ? 's' : ''}`
|
||||
: 'Done! Content generation complete';
|
||||
return { percentage: 100, friendlyMessage: finalMsg };
|
||||
} else if (isClustering) {
|
||||
const finalMsg = finalKeywordCount && finalClusterCount
|
||||
? `Done! Created ${finalClusterCount} cluster${finalClusterCount !== '1' ? 's' : ''} from ${finalKeywordCount} keyword${finalKeywordCount !== '1' ? 's' : ''}`
|
||||
: finalKeywordCount
|
||||
? `Done! Processed ${finalKeywordCount} keyword${finalKeywordCount !== '1' ? 's' : ''}`
|
||||
: finalClusterCount
|
||||
? `Done! Created ${finalClusterCount} cluster${finalClusterCount !== '1' ? 's' : ''}`
|
||||
: 'Done! Clustering complete';
|
||||
return { percentage: 100, friendlyMessage: finalMsg };
|
||||
} else if (isIdeas) {
|
||||
const finalMsg = finalTaskCount
|
||||
? `Done! Generated ${finalTaskCount} idea${finalTaskCount !== '1' ? 's' : ''}`
|
||||
: 'Done! Ideas generation complete';
|
||||
return { percentage: 100, friendlyMessage: finalMsg };
|
||||
}
|
||||
return { percentage: 100, friendlyMessage: 'Done! Task complete' };
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
@@ -200,7 +259,7 @@ export function useProgressModal(): UseProgressModalReturn {
|
||||
currentStep = 'AI_CALL';
|
||||
} else if (currentPhase.includes('pars') || currentMessage.includes('pars') || currentMessage.includes('organizing')) {
|
||||
currentStep = 'PARSE';
|
||||
} else if (currentPhase.includes('sav') || currentPhase.includes('creat') || currentMessage.includes('sav') || currentMessage.includes('creat') || currentMessage.includes('cluster')) {
|
||||
} else if (currentPhase.includes('sav') || currentPhase.includes('creat') || currentMessage.includes('sav') || currentMessage.includes('creat') || (currentMessage.includes('cluster') && !currentMessage.includes('content'))) {
|
||||
currentStep = 'SAVE';
|
||||
} else if (currentPhase.includes('done') || currentPhase.includes('complet') || currentMessage.includes('done') || currentMessage.includes('complet')) {
|
||||
currentStep = 'DONE';
|
||||
@@ -209,39 +268,64 @@ export function useProgressModal(): UseProgressModalReturn {
|
||||
|
||||
// Get step info with user-friendly message (use original message and all steps for value extraction)
|
||||
const originalMessage = meta.message || '';
|
||||
const stepInfo = getStepInfo(currentStep || '', originalMessage, allSteps);
|
||||
// Include title in message for better function type detection
|
||||
const messageWithContext = `${title} ${originalMessage}`;
|
||||
const stepInfo = getStepInfo(currentStep || '', messageWithContext, allSteps);
|
||||
const targetPercentage = stepInfo.percentage;
|
||||
const friendlyMessage = stepInfo.friendlyMessage;
|
||||
|
||||
// Check if we're transitioning to a new step
|
||||
const isNewStep = currentStepRef.current !== currentStep;
|
||||
const currentDisplayedPercentage = displayedPercentageRef.current;
|
||||
|
||||
// Clear any existing transition timeout
|
||||
// Clear any existing transition timeout or animation interval
|
||||
if (stepTransitionTimeoutRef.current) {
|
||||
clearTimeout(stepTransitionTimeoutRef.current);
|
||||
stepTransitionTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
// If it's a new step, add 500ms delay before updating
|
||||
if (isNewStep && currentStepRef.current !== null) {
|
||||
stepTransitionTimeoutRef.current = setTimeout(() => {
|
||||
// Smooth progress animation: increment by 1% every 300ms until reaching target
|
||||
if (targetPercentage > currentDisplayedPercentage) {
|
||||
// Start smooth animation
|
||||
let animatedPercentage = currentDisplayedPercentage;
|
||||
const animateProgress = () => {
|
||||
if (animatedPercentage < targetPercentage) {
|
||||
animatedPercentage = Math.min(animatedPercentage + 1, targetPercentage);
|
||||
displayedPercentageRef.current = animatedPercentage;
|
||||
setProgress({
|
||||
percentage: animatedPercentage,
|
||||
message: friendlyMessage,
|
||||
status: 'processing',
|
||||
details: {
|
||||
current: meta.current || 0,
|
||||
total: meta.total || 0,
|
||||
completed: meta.completed || 0,
|
||||
currentItem: meta.current_item,
|
||||
phase: meta.phase,
|
||||
},
|
||||
});
|
||||
|
||||
if (animatedPercentage < targetPercentage) {
|
||||
stepTransitionTimeoutRef.current = setTimeout(animateProgress, 300);
|
||||
} else {
|
||||
stepTransitionTimeoutRef.current = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// If it's a new step, add 500ms delay before starting animation
|
||||
if (isNewStep && currentStepRef.current !== null) {
|
||||
stepTransitionTimeoutRef.current = setTimeout(() => {
|
||||
currentStepRef.current = currentStep;
|
||||
animateProgress();
|
||||
}, 500);
|
||||
} else {
|
||||
// Same step or first step - start animation immediately
|
||||
currentStepRef.current = currentStep;
|
||||
displayedPercentageRef.current = targetPercentage;
|
||||
setProgress({
|
||||
percentage: targetPercentage,
|
||||
message: friendlyMessage,
|
||||
status: 'processing',
|
||||
details: {
|
||||
current: meta.current || 0,
|
||||
total: meta.total || 0,
|
||||
completed: meta.completed || 0,
|
||||
currentItem: meta.current_item,
|
||||
phase: meta.phase,
|
||||
},
|
||||
});
|
||||
}, 500);
|
||||
animateProgress();
|
||||
}
|
||||
} else {
|
||||
// Same step or first step - update immediately
|
||||
// Percentage decreased or same - update immediately (shouldn't happen normally)
|
||||
currentStepRef.current = currentStep;
|
||||
displayedPercentageRef.current = targetPercentage;
|
||||
setProgress({
|
||||
@@ -434,11 +518,14 @@ export function useProgressModal(): UseProgressModalReturn {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
// Clear step transition timeout on cleanup
|
||||
// Clear step transition timeout and animation on cleanup
|
||||
if (stepTransitionTimeoutRef.current) {
|
||||
clearTimeout(stepTransitionTimeoutRef.current);
|
||||
stepTransitionTimeoutRef.current = null;
|
||||
}
|
||||
// Reset displayed percentage
|
||||
displayedPercentageRef.current = 0;
|
||||
currentStepRef.current = null;
|
||||
};
|
||||
}, [taskId, isOpen]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user