815 lines
33 KiB
TypeScript
815 lines
33 KiB
TypeScript
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
import { fetchAPI } from '../services/api';
|
|
|
|
export interface ProgressState {
|
|
percentage: number;
|
|
message: string;
|
|
status: 'pending' | 'processing' | 'completed' | 'error';
|
|
details?: {
|
|
current: number;
|
|
total: number;
|
|
completed: number;
|
|
currentItem?: string;
|
|
phase?: string;
|
|
};
|
|
}
|
|
|
|
export interface ImageQueueItem {
|
|
image_id: number;
|
|
index: number;
|
|
label: string;
|
|
content_title: string;
|
|
status: 'pending' | 'processing' | 'completed' | 'failed';
|
|
progress: number;
|
|
image_url: string | null;
|
|
error: string | null;
|
|
}
|
|
|
|
export interface UseProgressModalReturn {
|
|
progress: ProgressState;
|
|
isOpen: boolean;
|
|
title: string;
|
|
taskId: string | null;
|
|
openModal: (taskId: string, title: string, functionId?: string) => void;
|
|
updateTaskId: (taskId: string) => void;
|
|
closeModal: () => void;
|
|
setError: (errorMessage: string) => void;
|
|
reset: () => void;
|
|
functionId?: string; // AI function ID for tracking
|
|
stepLogs: Array<{
|
|
stepNumber: number;
|
|
stepName: string;
|
|
status: string;
|
|
message: string;
|
|
timestamp?: number;
|
|
}>; // Step logs for debugging
|
|
imageQueue?: ImageQueueItem[]; // Image queue for image generation
|
|
}
|
|
|
|
export function useProgressModal(): UseProgressModalReturn {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [taskId, setTaskId] = useState<string | null>(null);
|
|
const [title, setTitle] = useState('');
|
|
const [functionId, setFunctionId] = useState<string | undefined>(undefined);
|
|
const [progress, setProgress] = useState<ProgressState>({
|
|
percentage: 0,
|
|
message: 'Initializing...',
|
|
status: 'pending',
|
|
});
|
|
|
|
// Step logs state for debugging
|
|
const [stepLogs, setStepLogs] = useState<Array<{
|
|
stepNumber: number;
|
|
stepName: string;
|
|
status: string;
|
|
message: string;
|
|
timestamp?: number;
|
|
}>>([]);
|
|
|
|
// Image queue state for image generation
|
|
const [imageQueue, setImageQueue] = useState<ImageQueueItem[] | undefined>(undefined);
|
|
|
|
// Track displayed percentage and current step for step-based progress
|
|
const displayedPercentageRef = useRef(0);
|
|
const currentStepRef = useRef<string | null>(null);
|
|
const stepTransitionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
const autoIncrementIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
// 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 => {
|
|
const match = text.match(pattern);
|
|
return match && match[1] ? match[1] : '';
|
|
};
|
|
|
|
// 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 && !taskCount && !itemCount) {
|
|
const loadedMatch = extractNumber(/loaded\s+(\d+)\s+items?/i, message);
|
|
if (loadedMatch) itemCount = loadedMatch;
|
|
}
|
|
if (!clusterCount) {
|
|
const createdMatch = extractNumber(/created\s+(\d+)\s+clusters?/i, message);
|
|
if (createdMatch) clusterCount = createdMatch;
|
|
}
|
|
|
|
// Check all steps if not found in message
|
|
if (!keywordCount && !taskCount && !itemCount) {
|
|
for (const step of allSteps) {
|
|
const stepMsg = step.message || '';
|
|
if (!keywordCount) {
|
|
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 || 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')) {
|
|
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')) {
|
|
if (isContentGeneration) {
|
|
return { percentage: 50, friendlyMessage: 'Writing article with Igny8 Semantic AI...' };
|
|
} else if (isClustering) {
|
|
return { percentage: 50, friendlyMessage: 'Generating clusters with Igny8 Semantic SEO Model...' };
|
|
} else if (isIdeas) {
|
|
return { percentage: 50, friendlyMessage: 'Generating ideas with Igny8 Semantic AI...' };
|
|
}
|
|
return { percentage: 50, friendlyMessage: 'Processing with Igny8 Semantic AI...' };
|
|
}
|
|
if (stepUpper.includes('PARSE') || stepUpper.includes('PARSING')) {
|
|
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') && !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')) {
|
|
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
|
|
return { percentage: displayedPercentageRef.current, friendlyMessage: message || 'Processing...' };
|
|
};
|
|
|
|
// Poll task status
|
|
useEffect(() => {
|
|
if (!taskId || !isOpen) return;
|
|
|
|
// Don't poll for temporary task IDs (they start with "temp-")
|
|
if (taskId.startsWith('temp-')) return;
|
|
|
|
let intervalId: NodeJS.Timeout | null = null;
|
|
let pollCount = 0;
|
|
let isStopped = false; // Flag to prevent multiple stops
|
|
const maxPolls = 300; // 10 minutes max (2 seconds * 300)
|
|
|
|
const pollTaskStatus = async () => {
|
|
// Don't poll if already stopped
|
|
if (isStopped) return;
|
|
|
|
try {
|
|
pollCount++;
|
|
|
|
// Check if we've exceeded max polls
|
|
if (pollCount > maxPolls) {
|
|
setProgress({
|
|
percentage: 0,
|
|
message: 'Task is taking longer than expected. Please check manually.',
|
|
status: 'error',
|
|
});
|
|
isStopped = true;
|
|
if (intervalId) {
|
|
clearInterval(intervalId);
|
|
intervalId = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const response = await fetchAPI(
|
|
`/v1/system/settings/task_progress/${taskId}/`
|
|
);
|
|
|
|
// Helper function to start auto-increment progress (1% every 350ms until 80%)
|
|
// Only runs when no backend updates are coming (smooth fill-in animation)
|
|
const startAutoIncrement = () => {
|
|
// Clear any existing auto-increment interval
|
|
if (autoIncrementIntervalRef.current) {
|
|
clearInterval(autoIncrementIntervalRef.current);
|
|
autoIncrementIntervalRef.current = null;
|
|
}
|
|
|
|
// Only start if we're below 80% and status is processing
|
|
const current = displayedPercentageRef.current;
|
|
if (current < 80) {
|
|
// Use a slightly longer interval to avoid conflicts with backend updates
|
|
autoIncrementIntervalRef.current = setInterval(() => {
|
|
setProgress(prev => {
|
|
// Check current status - stop if not processing
|
|
if (prev.status !== 'processing') {
|
|
if (autoIncrementIntervalRef.current) {
|
|
clearInterval(autoIncrementIntervalRef.current);
|
|
autoIncrementIntervalRef.current = null;
|
|
}
|
|
return prev;
|
|
}
|
|
|
|
const currentPercent = displayedPercentageRef.current;
|
|
// Only increment if still below 80%
|
|
if (currentPercent < 80) {
|
|
const newPercentage = Math.min(currentPercent + 1, 80);
|
|
displayedPercentageRef.current = newPercentage;
|
|
|
|
// Stop if we've reached 80%
|
|
if (newPercentage >= 80) {
|
|
if (autoIncrementIntervalRef.current) {
|
|
clearInterval(autoIncrementIntervalRef.current);
|
|
autoIncrementIntervalRef.current = null;
|
|
}
|
|
}
|
|
|
|
return {
|
|
...prev,
|
|
percentage: newPercentage,
|
|
};
|
|
} else {
|
|
// Stop if we've reached 80%
|
|
if (autoIncrementIntervalRef.current) {
|
|
clearInterval(autoIncrementIntervalRef.current);
|
|
autoIncrementIntervalRef.current = null;
|
|
}
|
|
return prev;
|
|
}
|
|
});
|
|
}, 350); // Slightly longer interval to reduce conflicts
|
|
}
|
|
};
|
|
|
|
if (response.state === 'PROGRESS') {
|
|
const meta = response.meta || {};
|
|
|
|
// Determine current step from request_steps/response_steps first (most accurate)
|
|
let currentStep: string | null = null;
|
|
const allSteps = [...(meta.request_steps || []), ...(meta.response_steps || [])];
|
|
|
|
// Get the latest step name from steps array
|
|
if (allSteps.length > 0) {
|
|
// Sort by stepNumber to get the latest
|
|
const sortedSteps = [...allSteps].sort((a: any, b: any) => (b.stepNumber || 0) - (a.stepNumber || 0));
|
|
const latestStep = sortedSteps[0];
|
|
if (latestStep && latestStep.stepName) {
|
|
const stepNameUpper = latestStep.stepName.toUpperCase();
|
|
// Map step names to standard step names
|
|
if (stepNameUpper.includes('INIT')) {
|
|
currentStep = 'INIT';
|
|
} else if (stepNameUpper.includes('PREP')) {
|
|
currentStep = 'PREP';
|
|
} else if (stepNameUpper.includes('AI_CALL') || stepNameUpper.includes('CALL')) {
|
|
currentStep = 'AI_CALL';
|
|
} else if (stepNameUpper.includes('PARSE')) {
|
|
currentStep = 'PARSE';
|
|
} else if (stepNameUpper.includes('SAVE') || stepNameUpper.includes('CREAT')) {
|
|
currentStep = 'SAVE';
|
|
} else if (stepNameUpper.includes('DONE') || stepNameUpper.includes('COMPLETE')) {
|
|
currentStep = 'DONE';
|
|
} else {
|
|
currentStep = stepNameUpper;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to phase or message if no step found
|
|
if (!currentStep) {
|
|
const currentPhase = (meta.phase || '').toUpperCase();
|
|
const currentMessage = (meta.message || '').toLowerCase();
|
|
|
|
// Check exact phase match first (backend now sends uppercase phase names)
|
|
if (currentPhase === 'INIT' || currentPhase.includes('INIT')) {
|
|
currentStep = 'INIT';
|
|
} else if (currentPhase === 'PREP' || currentPhase.includes('PREP')) {
|
|
currentStep = 'PREP';
|
|
} else if (currentPhase === 'AI_CALL' || currentPhase.includes('AI_CALL') || currentPhase.includes('CALL')) {
|
|
currentStep = 'AI_CALL';
|
|
} else if (currentPhase === 'PARSE' || currentPhase.includes('PARSE')) {
|
|
currentStep = 'PARSE';
|
|
} else if (currentPhase === 'SAVE' || currentPhase.includes('SAVE') || currentPhase.includes('CREAT')) {
|
|
currentStep = 'SAVE';
|
|
} else if (currentPhase === 'DONE' || currentPhase.includes('DONE') || currentPhase.includes('COMPLETE')) {
|
|
currentStep = 'DONE';
|
|
} else {
|
|
// Fallback to message-based detection
|
|
if (currentMessage.includes('initializ') || currentMessage.includes('getting started')) {
|
|
currentStep = 'INIT';
|
|
} else if (currentMessage.includes('prepar') || currentMessage.includes('loading')) {
|
|
currentStep = 'PREP';
|
|
} else if (currentMessage.includes('generating') || currentMessage.includes('analyzing') || currentMessage.includes('finding related')) {
|
|
currentStep = 'AI_CALL';
|
|
} else if (currentMessage.includes('pars') || currentMessage.includes('organizing') || currentMessage.includes('processing content')) {
|
|
currentStep = 'PARSE';
|
|
} else if (currentMessage.includes('sav') || currentMessage.includes('creat') || (currentMessage.includes('cluster') && !currentMessage.includes('content'))) {
|
|
currentStep = 'SAVE';
|
|
} else if (currentMessage.includes('done') || currentMessage.includes('complet')) {
|
|
currentStep = 'DONE';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get step info with user-friendly message (use original message and all steps for value extraction)
|
|
const originalMessage = meta.message || '';
|
|
// Include title in message for better function type detection
|
|
const messageWithContext = `${title} ${originalMessage}`;
|
|
const stepInfo = getStepInfo(currentStep || '', messageWithContext, allSteps);
|
|
// Use backend percentage if available, otherwise use step-based percentage
|
|
const backendPercentage = meta.percentage !== undefined ? meta.percentage : null;
|
|
const targetPercentage = backendPercentage !== null ? backendPercentage : stepInfo.percentage;
|
|
const friendlyMessage = stepInfo.friendlyMessage;
|
|
|
|
// Check if we're transitioning to a new step
|
|
const isNewStep = currentStepRef.current !== currentStep;
|
|
const currentDisplayedPercentage = displayedPercentageRef.current;
|
|
|
|
// Stop auto-increment when backend sends an update (prevents conflicts)
|
|
if (autoIncrementIntervalRef.current) {
|
|
clearInterval(autoIncrementIntervalRef.current);
|
|
autoIncrementIntervalRef.current = null;
|
|
}
|
|
|
|
// Clear any existing transition timeout or animation interval
|
|
if (stepTransitionTimeoutRef.current) {
|
|
clearTimeout(stepTransitionTimeoutRef.current);
|
|
stepTransitionTimeoutRef.current = null;
|
|
}
|
|
|
|
// Improved progress logic: Only allow progress to increase, never decrease
|
|
// This prevents jumping back and forth between percentages
|
|
const safeTargetPercentage = Math.max(targetPercentage, currentDisplayedPercentage);
|
|
|
|
// Smooth progress animation: increment gradually until reaching target
|
|
// Use smaller increments and faster updates for smoother animation
|
|
if (safeTargetPercentage > currentDisplayedPercentage) {
|
|
// Start smooth animation
|
|
let animatedPercentage = currentDisplayedPercentage;
|
|
const animateProgress = () => {
|
|
if (animatedPercentage < safeTargetPercentage) {
|
|
// Calculate increment based on distance for smooth animation
|
|
const diff = safeTargetPercentage - animatedPercentage;
|
|
// Use smaller increments for smoother feel
|
|
// If close (< 5%), increment by 1, otherwise by 2
|
|
const increment = diff <= 5 ? 1 : Math.min(2, Math.ceil(diff / 10));
|
|
animatedPercentage = Math.min(animatedPercentage + increment, safeTargetPercentage);
|
|
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 < safeTargetPercentage) {
|
|
// Smooth updates: 150ms for better UX
|
|
stepTransitionTimeoutRef.current = setTimeout(animateProgress, 150);
|
|
} else {
|
|
stepTransitionTimeoutRef.current = null;
|
|
// After reaching target, start auto-increment if below 80% and no backend update pending
|
|
if (safeTargetPercentage < 80) {
|
|
startAutoIncrement();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// If it's a new step, add small delay before starting animation
|
|
if (isNewStep && currentStepRef.current !== null) {
|
|
stepTransitionTimeoutRef.current = setTimeout(() => {
|
|
currentStepRef.current = currentStep;
|
|
animateProgress();
|
|
}, 200);
|
|
} else {
|
|
// Same step or first step - start animation immediately
|
|
currentStepRef.current = currentStep;
|
|
animateProgress();
|
|
}
|
|
} else {
|
|
// Target is same or less than current - just update message and details
|
|
// Don't decrease percentage (prevents jumping back)
|
|
currentStepRef.current = currentStep;
|
|
setProgress(prev => ({
|
|
...prev,
|
|
message: friendlyMessage,
|
|
details: {
|
|
current: meta.current || 0,
|
|
total: meta.total || 0,
|
|
completed: meta.completed || 0,
|
|
currentItem: meta.current_item,
|
|
phase: meta.phase,
|
|
},
|
|
}));
|
|
// Start auto-increment if below 80% and no backend update
|
|
if (currentDisplayedPercentage < 80 && safeTargetPercentage === currentDisplayedPercentage) {
|
|
startAutoIncrement();
|
|
}
|
|
}
|
|
|
|
// Extract image queue if available (for image generation)
|
|
if (meta.image_queue && Array.isArray(meta.image_queue)) {
|
|
setImageQueue(meta.image_queue as ImageQueueItem[]);
|
|
}
|
|
|
|
// Update step logs if available
|
|
if (meta.request_steps || meta.response_steps) {
|
|
// Collect all steps for display in modal
|
|
const allSteps: Array<{
|
|
stepNumber: number;
|
|
stepName: string;
|
|
status: string;
|
|
message: string;
|
|
timestamp?: number;
|
|
}> = [];
|
|
|
|
if (meta.request_steps && Array.isArray(meta.request_steps)) {
|
|
meta.request_steps.forEach((step: any) => {
|
|
allSteps.push({
|
|
stepNumber: step.stepNumber || 0,
|
|
stepName: step.stepName || 'Unknown',
|
|
status: step.status || 'success',
|
|
message: step.message || '',
|
|
timestamp: step.timestamp,
|
|
});
|
|
});
|
|
}
|
|
|
|
if (meta.response_steps && Array.isArray(meta.response_steps)) {
|
|
meta.response_steps.forEach((step: any) => {
|
|
allSteps.push({
|
|
stepNumber: step.stepNumber || 0,
|
|
stepName: step.stepName || 'Unknown',
|
|
status: step.status || 'success',
|
|
message: step.message || '',
|
|
timestamp: step.timestamp,
|
|
});
|
|
});
|
|
}
|
|
|
|
// Sort by step number and update state
|
|
allSteps.sort((a, b) => a.stepNumber - b.stepNumber);
|
|
setStepLogs(allSteps);
|
|
}
|
|
} else if (response.state === 'SUCCESS') {
|
|
const meta = response.meta || {};
|
|
|
|
// Clear any existing transition timeout
|
|
if (stepTransitionTimeoutRef.current) {
|
|
clearTimeout(stepTransitionTimeoutRef.current);
|
|
stepTransitionTimeoutRef.current = null;
|
|
}
|
|
|
|
// Get completion message with extracted values
|
|
const completionMessage = meta.message || '';
|
|
const allSteps = [...(meta.request_steps || []), ...(meta.response_steps || [])];
|
|
const stepInfo = getStepInfo('DONE', completionMessage, allSteps);
|
|
|
|
// Update to 100% with user-friendly completion message
|
|
currentStepRef.current = 'DONE';
|
|
displayedPercentageRef.current = 100;
|
|
setProgress({
|
|
percentage: 100,
|
|
message: stepInfo.friendlyMessage,
|
|
status: 'completed',
|
|
details: meta.details,
|
|
});
|
|
|
|
// Update final step logs
|
|
if (meta.request_steps || meta.response_steps) {
|
|
// Collect all steps for display in modal
|
|
const allSteps: Array<{
|
|
stepNumber: number;
|
|
stepName: string;
|
|
status: string;
|
|
message: string;
|
|
timestamp?: number;
|
|
}> = [];
|
|
|
|
if (meta.request_steps && Array.isArray(meta.request_steps)) {
|
|
meta.request_steps.forEach((step: any) => {
|
|
allSteps.push({
|
|
stepNumber: step.stepNumber || 0,
|
|
stepName: step.stepName || 'Unknown',
|
|
status: step.status || 'success',
|
|
message: step.message || '',
|
|
timestamp: step.timestamp,
|
|
});
|
|
});
|
|
}
|
|
|
|
if (meta.response_steps && Array.isArray(meta.response_steps)) {
|
|
meta.response_steps.forEach((step: any) => {
|
|
allSteps.push({
|
|
stepNumber: step.stepNumber || 0,
|
|
stepName: step.stepName || 'Unknown',
|
|
status: step.status || 'success',
|
|
message: step.message || '',
|
|
timestamp: step.timestamp,
|
|
});
|
|
});
|
|
}
|
|
|
|
// Sort by step number and update state
|
|
allSteps.sort((a, b) => a.stepNumber - b.stepNumber);
|
|
setStepLogs(allSteps);
|
|
}
|
|
|
|
// Stop polling on SUCCESS
|
|
isStopped = true;
|
|
if (intervalId) {
|
|
clearInterval(intervalId);
|
|
intervalId = null;
|
|
}
|
|
} else if (response.state === 'FAILURE') {
|
|
const meta = response.meta || {};
|
|
// Try multiple error message sources
|
|
const errorMsg = meta.error || meta.message || response.error || 'Task failed - exception details unavailable';
|
|
const errorType = meta.error_type || 'Error';
|
|
setProgress({
|
|
percentage: 0,
|
|
message: errorMsg.includes('exception details unavailable') ? errorMsg : `Error: ${errorMsg}`,
|
|
status: 'error',
|
|
details: meta.error_type ? `${errorType}: ${errorMsg}` : errorMsg,
|
|
});
|
|
|
|
// Update step logs from failure response
|
|
if (meta.request_steps || meta.response_steps) {
|
|
const allSteps: Array<{
|
|
stepNumber: number;
|
|
stepName: string;
|
|
status: string;
|
|
message: string;
|
|
timestamp?: number;
|
|
}> = [];
|
|
|
|
if (meta.request_steps && Array.isArray(meta.request_steps)) {
|
|
meta.request_steps.forEach((step: any) => {
|
|
allSteps.push({
|
|
stepNumber: step.stepNumber || 0,
|
|
stepName: step.stepName || 'Unknown',
|
|
status: step.status || 'error',
|
|
message: step.message || '',
|
|
timestamp: step.timestamp,
|
|
});
|
|
});
|
|
}
|
|
|
|
if (meta.response_steps && Array.isArray(meta.response_steps)) {
|
|
meta.response_steps.forEach((step: any) => {
|
|
allSteps.push({
|
|
stepNumber: step.stepNumber || 0,
|
|
stepName: step.stepName || 'Unknown',
|
|
status: step.status || 'error',
|
|
message: step.message || '',
|
|
timestamp: step.timestamp,
|
|
});
|
|
});
|
|
}
|
|
|
|
allSteps.sort((a, b) => a.stepNumber - b.stepNumber);
|
|
setStepLogs(allSteps);
|
|
}
|
|
|
|
// Stop polling on FAILURE
|
|
isStopped = true;
|
|
if (intervalId) {
|
|
clearInterval(intervalId);
|
|
intervalId = null;
|
|
}
|
|
} else {
|
|
// PENDING or other states
|
|
setProgress({
|
|
percentage: 0,
|
|
message: 'Task is starting...',
|
|
status: 'pending',
|
|
});
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Error polling task status:', error);
|
|
// Continue polling on error (might be temporary)
|
|
// Only show error after multiple consecutive failures
|
|
if (pollCount > 5) {
|
|
// Extract clean error message
|
|
let errorMsg = error.message || 'Unknown error';
|
|
if (errorMsg.includes('HTTP_ERROR')) {
|
|
// Try to extract the actual error from the response
|
|
errorMsg = errorMsg.replace(/^API Error \(\d+\): HTTP_ERROR - /, '').trim() || 'Server error';
|
|
}
|
|
setProgress({
|
|
percentage: 0,
|
|
message: `Error checking task status: ${errorMsg}`,
|
|
status: 'error',
|
|
});
|
|
// Stop polling after showing error
|
|
isStopped = true;
|
|
if (intervalId) {
|
|
clearInterval(intervalId);
|
|
intervalId = null;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Start polling immediately, then every 2 seconds
|
|
pollTaskStatus();
|
|
intervalId = setInterval(() => {
|
|
if (!isStopped) {
|
|
pollTaskStatus();
|
|
}
|
|
}, 2000);
|
|
|
|
return () => {
|
|
isStopped = true;
|
|
if (intervalId) {
|
|
clearInterval(intervalId);
|
|
intervalId = null;
|
|
}
|
|
// Clear step transition timeout and animation on cleanup
|
|
if (stepTransitionTimeoutRef.current) {
|
|
clearTimeout(stepTransitionTimeoutRef.current);
|
|
stepTransitionTimeoutRef.current = null;
|
|
}
|
|
// Clear auto-increment interval
|
|
if (autoIncrementIntervalRef.current) {
|
|
clearInterval(autoIncrementIntervalRef.current);
|
|
autoIncrementIntervalRef.current = null;
|
|
}
|
|
// Reset displayed percentage
|
|
displayedPercentageRef.current = 0;
|
|
currentStepRef.current = null;
|
|
};
|
|
}, [taskId, isOpen]);
|
|
|
|
const openModal = useCallback((newTaskId: string, newTitle: string, newFunctionId?: string) => {
|
|
// Clear any existing transition timeout
|
|
if (stepTransitionTimeoutRef.current) {
|
|
clearTimeout(stepTransitionTimeoutRef.current);
|
|
stepTransitionTimeoutRef.current = null;
|
|
}
|
|
// Clear auto-increment interval
|
|
if (autoIncrementIntervalRef.current) {
|
|
clearInterval(autoIncrementIntervalRef.current);
|
|
autoIncrementIntervalRef.current = null;
|
|
}
|
|
displayedPercentageRef.current = 0;
|
|
currentStepRef.current = null;
|
|
setStepLogs([]); // Reset step logs when opening modal
|
|
setTaskId(newTaskId);
|
|
setTitle(newTitle);
|
|
setFunctionId(newFunctionId);
|
|
setIsOpen(true);
|
|
setProgress({
|
|
percentage: 0,
|
|
message: 'Getting started...',
|
|
status: 'pending',
|
|
});
|
|
}, []);
|
|
|
|
const updateTaskId = useCallback((newTaskId: string) => {
|
|
setTaskId(newTaskId);
|
|
// Reset progress when updating to real task ID
|
|
setProgress({
|
|
percentage: 0,
|
|
message: 'Initializing...',
|
|
status: 'pending',
|
|
});
|
|
}, []);
|
|
|
|
const closeModal = useCallback(() => {
|
|
// Clear any existing transition timeout
|
|
if (stepTransitionTimeoutRef.current) {
|
|
clearTimeout(stepTransitionTimeoutRef.current);
|
|
stepTransitionTimeoutRef.current = null;
|
|
}
|
|
// Clear auto-increment interval
|
|
if (autoIncrementIntervalRef.current) {
|
|
clearInterval(autoIncrementIntervalRef.current);
|
|
autoIncrementIntervalRef.current = null;
|
|
}
|
|
displayedPercentageRef.current = 0;
|
|
currentStepRef.current = null;
|
|
setStepLogs([]); // Clear step logs when closing modal
|
|
setImageQueue(undefined); // Clear image queue when closing modal
|
|
setIsOpen(false);
|
|
// Clear taskId to stop polling when modal closes
|
|
setTaskId(null);
|
|
setTitle('');
|
|
}, []);
|
|
|
|
const setError = useCallback((errorMessage: string) => {
|
|
setProgress({
|
|
percentage: 0,
|
|
message: errorMessage,
|
|
status: 'error',
|
|
});
|
|
}, []);
|
|
|
|
const reset = useCallback(() => {
|
|
// Clear any existing transition timeout
|
|
if (stepTransitionTimeoutRef.current) {
|
|
clearTimeout(stepTransitionTimeoutRef.current);
|
|
stepTransitionTimeoutRef.current = null;
|
|
}
|
|
// Clear auto-increment interval
|
|
if (autoIncrementIntervalRef.current) {
|
|
clearInterval(autoIncrementIntervalRef.current);
|
|
autoIncrementIntervalRef.current = null;
|
|
}
|
|
displayedPercentageRef.current = 0;
|
|
currentStepRef.current = null;
|
|
setProgress({
|
|
percentage: 0,
|
|
message: 'Getting started...',
|
|
status: 'pending',
|
|
});
|
|
setStepLogs([]);
|
|
setImageQueue(undefined);
|
|
setTaskId(null);
|
|
setTitle('');
|
|
setIsOpen(false);
|
|
}, []);
|
|
|
|
return {
|
|
progress,
|
|
isOpen,
|
|
openModal,
|
|
updateTaskId,
|
|
closeModal,
|
|
setError,
|
|
reset,
|
|
title, // Expose title for use in component
|
|
taskId, // Expose taskId for use in ProgressModal
|
|
functionId, // Expose functionId for use in ProgressModal
|
|
stepLogs, // Expose step logs for debugging
|
|
imageQueue, // Expose image queue for image generation
|
|
};
|
|
}
|
|
|