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 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 } export function useProgressModal(): UseProgressModalReturn { const [isOpen, setIsOpen] = useState(false); const [taskId, setTaskId] = useState(null); const [title, setTitle] = useState(''); const [functionId, setFunctionId] = useState(undefined); const [progress, setProgress] = useState({ percentage: 0, message: 'Initializing...', status: 'pending', }); // Step logs state for debugging const [stepLogs, setStepLogs] = useState>([]); // Track displayed percentage and current step for step-based progress const displayedPercentageRef = useRef(0); const currentStepRef = useRef(null); const stepTransitionTimeoutRef = useRef(null); const autoIncrementIntervalRef = useRef(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: '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')) { 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(); } } // 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); // Also update AI request logs store const { useAIRequestLogsStore } = await import('../store/aiRequestLogsStore').catch(() => ({ useAIRequestLogsStore: null })); const logs = useAIRequestLogsStore?.getState().logs || []; const log = logs.find(l => l.response?.data?.task_id === taskId); if (log) { const addRequestStep = useAIRequestLogsStore?.getState().addRequestStep; const addResponseStep = useAIRequestLogsStore?.getState().addResponseStep; if (meta.request_steps && Array.isArray(meta.request_steps)) { meta.request_steps.forEach((step: any) => { // Only add if not already present if (!log.requestSteps.find(s => s.stepNumber === step.stepNumber)) { addRequestStep?.(log.id, step); } }); } if (meta.response_steps && Array.isArray(meta.response_steps)) { meta.response_steps.forEach((step: any) => { // Only add if not already present if (!log.responseSteps.find(s => s.stepNumber === step.stepNumber)) { addResponseStep?.(log.id, step); } }); } } } } 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); // Also update AI request logs store const { useAIRequestLogsStore } = await import('../store/aiRequestLogsStore').catch(() => ({ useAIRequestLogsStore: null })); const logs = useAIRequestLogsStore?.getState().logs || []; // Find log by task_id in response data or by matching the most recent log const log = logs.find(l => l.response?.data?.task_id === taskId) || logs[0]; if (log) { const addRequestStep = useAIRequestLogsStore?.getState().addRequestStep; const addResponseStep = useAIRequestLogsStore?.getState().addResponseStep; if (meta.request_steps && Array.isArray(meta.request_steps)) { meta.request_steps.forEach((step: any) => { if (!log.requestSteps.find(s => s.stepNumber === step.stepNumber)) { addRequestStep?.(log.id, step); } }); } if (meta.response_steps && Array.isArray(meta.response_steps)) { meta.response_steps.forEach((step: any) => { if (!log.responseSteps.find(s => s.stepNumber === step.stepNumber)) { addResponseStep?.(log.id, step); } }); } } } // 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 { useAIRequestLogsStore } = await import('../store/aiRequestLogsStore').catch(() => ({ useAIRequestLogsStore: null })); const logs = useAIRequestLogsStore?.getState().logs || []; const log = logs.find(l => l.response?.data?.task_id === taskId); if (log) { const addRequestStep = useAIRequestLogsStore?.getState().addRequestStep; const addResponseStep = useAIRequestLogsStore?.getState().addResponseStep; if (meta.request_steps && Array.isArray(meta.request_steps)) { meta.request_steps.forEach((step: any) => { if (!log.requestSteps.find(s => s.stepNumber === step.stepNumber)) { addRequestStep?.(log.id, step); } }); } if (meta.response_steps && Array.isArray(meta.response_steps)) { meta.response_steps.forEach((step: any) => { if (!log.responseSteps.find(s => s.stepNumber === step.stepNumber)) { addResponseStep?.(log.id, step); } }); } } } // 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 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', }); 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 }; }