Initial commit: igny8 project

This commit is contained in:
igny8
2025-11-09 10:27:02 +00:00
commit 60b8188111
27265 changed files with 4360521 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
import { useState, useCallback, useEffect } from 'react';
interface ErrorInfo {
message: string;
source: string;
timestamp: number;
stack?: string;
}
const globalErrors: ErrorInfo[] = [];
const listeners = new Set<(errors: ErrorInfo[]) => void>();
export function useErrorHandler(componentName: string) {
const [errors, setErrors] = useState<ErrorInfo[]>([]);
useEffect(() => {
const updateErrors = () => {
setErrors([...globalErrors]);
};
listeners.add(updateErrors);
updateErrors();
return () => {
listeners.delete(updateErrors);
};
}, []);
const addError = useCallback((error: Error | string, source?: string) => {
const errorInfo: ErrorInfo = {
message: error instanceof Error ? error.message : error,
source: source || componentName,
timestamp: Date.now(),
stack: error instanceof Error ? error.stack : undefined,
};
globalErrors.push(errorInfo);
// Keep only last 10 errors
if (globalErrors.length > 10) {
globalErrors.shift();
}
listeners.forEach(listener => listener([...globalErrors]));
console.error(`[${errorInfo.source}]`, errorInfo);
}, [componentName]);
const clearError = useCallback((index: number) => {
globalErrors.splice(index, 1);
listeners.forEach(listener => listener([...globalErrors]));
}, []);
const clearAllErrors = useCallback(() => {
globalErrors.length = 0;
listeners.forEach(listener => listener([]));
}, []);
return {
errors,
addError,
clearError,
clearAllErrors,
};
}

View File

@@ -0,0 +1,17 @@
import { useNavigate } from "react-router";
const useGoBack = () => {
const navigate = useNavigate();
const goBack = () => {
if (window.history.state && window.history.state.idx > 0) {
navigate(-1); // Go back to the previous page
} else {
navigate("/"); // Redirect to home if no history exists
}
};
return goBack;
};
export default useGoBack;

View File

@@ -0,0 +1,11 @@
import { useState, useCallback } from "react";
export const useModal = (initialState: boolean = false) => {
const [isOpen, setIsOpen] = useState(initialState);
const openModal = useCallback(() => setIsOpen(true), []);
const closeModal = useCallback(() => setIsOpen(false), []);
const toggleModal = useCallback(() => setIsOpen((prev) => !prev), []);
return { isOpen, openModal, closeModal, toggleModal };
};

View File

@@ -0,0 +1,73 @@
import { useState, useCallback, useEffect, useRef } from 'react';
import { useErrorHandler } from './useErrorHandler';
import { trackLoading } from '../components/common/LoadingStateMonitor';
interface UsePageDataLoaderOptions {
loadFunction: () => Promise<any>;
componentName: string;
autoLoad?: boolean;
dependencies?: any[];
}
export function usePageDataLoader<T = any>({
loadFunction,
componentName,
autoLoad = true,
dependencies = [],
}: UsePageDataLoaderOptions) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { addError } = useErrorHandler(componentName);
const loadingKey = `${componentName}-data-loading`;
const hasLoadedRef = useRef(false);
const load = useCallback(async () => {
setLoading(true);
setError(null);
trackLoading(loadingKey, true);
try {
const result = await loadFunction();
setData(result);
setError(null);
return result;
} catch (err: any) {
const errorMessage = err.message || 'Failed to load data';
setError(errorMessage);
addError(err, componentName);
throw err;
} finally {
setLoading(false);
trackLoading(loadingKey, false);
}
}, [loadFunction, componentName, loadingKey, addError]);
useEffect(() => {
if (autoLoad && !hasLoadedRef.current) {
hasLoadedRef.current = true;
load().catch(() => {
// Error already handled by addError
});
}
}, [autoLoad, load]);
// Reload when dependencies change
useEffect(() => {
if (autoLoad && hasLoadedRef.current && dependencies.length > 0) {
load().catch(() => {
// Error already handled by addError
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, dependencies);
return {
data,
loading,
error,
load,
reload: load,
};
}

View File

@@ -0,0 +1,236 @@
import { useState, useEffect, useCallback } from 'react';
import { fetchAPI } from '../services/api';
interface UsePersistentToggleOptions {
/**
* Unique identifier for the resource (e.g., 'openai', 'runware', 'gsc')
*/
resourceId: string;
/**
* API endpoint pattern - will replace {id} with resourceId
* Example: '/v1/system/settings/integrations/{id}/'
*/
getEndpoint: string;
/**
* API endpoint pattern for saving - will replace {id} with resourceId
* Example: '/v1/system/settings/integrations/{id}/save/'
*/
saveEndpoint: string;
/**
* Initial enabled state (used before data loads)
*/
initialEnabled?: boolean;
/**
* Function to extract enabled state from API response
* Default: (data) => data?.enabled ?? false
*/
extractEnabled?: (data: any) => boolean;
/**
* Function to build save payload
* Default: (currentData, enabled) => ({ ...currentData, enabled })
*/
buildPayload?: (currentData: any, enabled: boolean) => any;
/**
* Callback when toggle succeeds
* @param enabled - The new enabled state
* @param data - The full config data from the API
*/
onToggleSuccess?: (enabled: boolean, data?: any) => void;
/**
* Callback when toggle fails
*/
onToggleError?: (error: Error) => void;
/**
* Whether to load state on mount
*/
loadOnMount?: boolean;
}
interface UsePersistentToggleReturn {
/**
* Current enabled state
*/
enabled: boolean;
/**
* Toggle function - automatically persists to backend
*/
toggle: (enabled: boolean) => Promise<void>;
/**
* Loading state (during save/load operations)
*/
loading: boolean;
/**
* Error state
*/
error: Error | null;
/**
* Full config data from API
*/
data: any;
/**
* Manually refresh state from API
*/
refresh: () => Promise<void>;
}
/**
* Hook for managing persistent toggle state with automatic API synchronization
*
* Features:
* - Automatically loads state on mount
* - Automatically saves state on toggle
* - Handles loading and error states
* - Can be used for any persistent boolean state
*
* @example
* ```tsx
* const { enabled, toggle, loading } = usePersistentToggle({
* resourceId: 'openai',
* getEndpoint: '/v1/system/settings/integrations/{id}/',
* saveEndpoint: '/v1/system/settings/integrations/{id}/save/',
* onToggleSuccess: (enabled) => toast.success(`Integration ${enabled ? 'enabled' : 'disabled'}`),
* });
* ```
*/
export function usePersistentToggle(
options: UsePersistentToggleOptions
): UsePersistentToggleReturn {
const {
resourceId,
getEndpoint,
saveEndpoint,
initialEnabled = false,
extractEnabled = (data: any) => data?.enabled ?? false,
buildPayload = (currentData: any, enabled: boolean) => ({ ...currentData, enabled }),
onToggleSuccess,
onToggleError,
loadOnMount = true,
} = options;
const [enabled, setEnabled] = useState(initialEnabled);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [data, setData] = useState<any>(null);
/**
* Load state from API
*/
const loadState = useCallback(async () => {
setLoading(true);
setError(null);
try {
const endpoint = getEndpoint.replace('{id}', resourceId);
const result = await fetchAPI(endpoint);
if (result.success && result.data) {
const apiData = result.data;
setData(apiData);
const newEnabled = extractEnabled(apiData);
setEnabled(newEnabled);
} else {
// No data yet - use initial state
setData({});
setEnabled(initialEnabled);
}
} catch (err: any) {
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
console.error(`Error loading state for ${resourceId}:`, error);
// Don't throw - just log and keep initial state
} finally {
setLoading(false);
}
}, [resourceId, getEndpoint, extractEnabled, initialEnabled]);
/**
* Save state to API
*/
const saveState = useCallback(async (newEnabled: boolean) => {
setLoading(true);
setError(null);
try {
const endpoint = saveEndpoint.replace('{id}', resourceId);
const payload = buildPayload(data || {}, newEnabled);
const result = await fetchAPI(endpoint, {
method: 'POST',
body: JSON.stringify(payload),
});
if (result.success) {
// Update local state
const updatedData = { ...(data || {}), enabled: newEnabled };
setData(updatedData);
setEnabled(newEnabled);
// Call success callback - pass both enabled state and full config data
if (onToggleSuccess) {
onToggleSuccess(newEnabled, updatedData);
}
} else {
throw new Error(result.error || 'Failed to save state');
}
} catch (err: any) {
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
console.error(`Error saving state for ${resourceId}:`, error);
// Call error callback
if (onToggleError) {
onToggleError(error);
}
// Revert to previous state on error
// Don't throw - let component handle error display
} finally {
setLoading(false);
}
}, [resourceId, saveEndpoint, buildPayload, data, onToggleSuccess, onToggleError]);
/**
* Toggle function - automatically persists
*/
const toggle = useCallback(async (newEnabled: boolean) => {
await saveState(newEnabled);
}, [saveState]);
/**
* Refresh state from API
*/
const refresh = useCallback(async () => {
await loadState();
}, [loadState]);
// Load state on mount - only once, don't re-run when dependencies change
useEffect(() => {
if (loadOnMount) {
loadState();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loadOnMount]); // Only depend on loadOnMount, not loadState
return {
enabled,
toggle,
loading,
error,
data,
refresh,
};
}

View File

@@ -0,0 +1,525 @@
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) => void;
updateTaskId: (taskId: string) => void;
closeModal: () => void;
setError: (errorMessage: string) => void;
reset: () => void;
}
export function useProgressModal(): UseProgressModalReturn {
const [isOpen, setIsOpen] = useState(false);
const [taskId, setTaskId] = useState<string | null>(null);
const [title, setTitle] = useState('');
const [progress, setProgress] = useState<ProgressState>({
percentage: 0,
message: 'Initializing...',
status: 'pending',
});
// 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);
// Step mapping with user-friendly messages and percentages
const getStepInfo = (stepName: string, message: string = '', allSteps: any[] = []): { percentage: number; friendlyMessage: string } => {
const stepUpper = stepName?.toUpperCase() || '';
// 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);
// Also check for "Loaded X items" or "Created X clusters" patterns
if (!keywordCount) {
const loadedMatch = extractNumber(/loaded\s+(\d+)\s+items?/i, message);
if (loadedMatch) keywordCount = 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 || !clusterCount) {
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);
}
if (!clusterCount) {
clusterCount = extractNumber(/(\d+)\s+cluster/i, stepMsg) || extractNumber(/created\s+(\d+)\s+clusters?/i, stepMsg);
}
if (keywordCount && clusterCount) break;
}
}
const finalKeywordCount = keywordCount;
const finalClusterCount = clusterCount;
// 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 (stepUpper.includes('AI_CALL') || stepUpper.includes('CALLING')) {
return { percentage: 50, friendlyMessage: 'Finding related keywords...' };
}
if (stepUpper.includes('PARSE') || stepUpper.includes('PARSING')) {
return { percentage: 70, friendlyMessage: 'Organizing 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('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 };
}
// 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}/`
);
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 || '').toLowerCase();
const currentMessage = (meta.message || '').toLowerCase();
if (currentPhase.includes('initializ') || currentMessage.includes('initializ') || currentMessage.includes('getting started')) {
currentStep = 'INIT';
} else if (currentPhase.includes('prepar') || currentPhase.includes('prep') || currentMessage.includes('prepar') || currentMessage.includes('loading')) {
currentStep = 'PREP';
} else if (currentPhase.includes('analyzing') || currentPhase.includes('ai_call') || currentMessage.includes('analyzing') || currentMessage.includes('finding related')) {
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')) {
currentStep = 'SAVE';
} else if (currentPhase.includes('done') || currentPhase.includes('complet') || 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 || '';
const stepInfo = getStepInfo(currentStep || '', originalMessage, allSteps);
const targetPercentage = stepInfo.percentage;
const friendlyMessage = stepInfo.friendlyMessage;
// Check if we're transitioning to a new step
const isNewStep = currentStepRef.current !== currentStep;
// Clear any existing transition timeout
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(() => {
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);
} else {
// Same step or first step - update 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,
},
});
}
// Update step logs if available
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) => {
// 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) {
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 || {};
const errorMsg = meta.error || 'Task failed';
setProgress({
percentage: 0,
message: `Error: ${errorMsg}`,
status: 'error',
});
// 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 on cleanup
if (stepTransitionTimeoutRef.current) {
clearTimeout(stepTransitionTimeoutRef.current);
stepTransitionTimeoutRef.current = null;
}
};
}, [taskId, isOpen]);
const openModal = useCallback((newTaskId: string, newTitle: string) => {
// Clear any existing transition timeout
if (stepTransitionTimeoutRef.current) {
clearTimeout(stepTransitionTimeoutRef.current);
stepTransitionTimeoutRef.current = null;
}
displayedPercentageRef.current = 0;
currentStepRef.current = null;
setTaskId(newTaskId);
setTitle(newTitle);
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;
}
displayedPercentageRef.current = 0;
currentStepRef.current = null;
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;
}
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
};
}