Fix: Ajout de toutes les actions VWB à la liste de détection
Mise à jour KNOWN_VWB_ACTIONS pour inclure: - double_click_anchor, right_click_anchor, hover_anchor, drag_drop_anchor - keyboard_shortcut - ai_analyze_text - extract_table, download_to_folder - db_save_data, db_read_data - visual_condition, loop_visual Corrige le problème "Type d'étape non reconnu" pour ces actions. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,418 @@
|
|||||||
|
/**
|
||||||
|
* Hook d'intégration des étapes VWB - Gestion des actions du catalogue dans le workflow
|
||||||
|
* Auteur : Dom, Alice, Kiro - 12 janvier 2026
|
||||||
|
*
|
||||||
|
* Ce hook gère l'intégration complète des actions VisionOnly du catalogue
|
||||||
|
* dans le système de workflow, incluant la création, la configuration et l'exécution.
|
||||||
|
*
|
||||||
|
* Version 2.0 - Intégration avec StepTypeResolver pour une détection robuste
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useCallback, useMemo, useRef } from 'react';
|
||||||
|
import { catalogService } from '../services/catalogService';
|
||||||
|
import { VWBCatalogAction } from '../types/catalog';
|
||||||
|
import { Step, StepType, StepData, StepExecutionState } from '../types';
|
||||||
|
import { stepTypeResolver, StepTypeResolutionResult } from '../services/StepTypeResolver';
|
||||||
|
// Import du catalogue statique pour fallback direct
|
||||||
|
import { getStaticActionById, findActionWithFallback } from '../data/staticCatalog';
|
||||||
|
|
||||||
|
interface VWBStepIntegrationState {
|
||||||
|
vwbActions: Map<string, VWBCatalogAction>;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
resolutionCache: Map<string, StepTypeResolutionResult>;
|
||||||
|
lastCacheUpdate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VWBStepIntegrationMethods {
|
||||||
|
createVWBStep: (actionId: string, position: { x: number; y: number }) => Promise<Step | null>;
|
||||||
|
isVWBAction: (stepType: string) => boolean;
|
||||||
|
getVWBAction: (actionId: string) => VWBCatalogAction | null;
|
||||||
|
loadVWBAction: (actionId: string) => Promise<VWBCatalogAction | null>;
|
||||||
|
validateVWBStep: (step: Step) => Promise<boolean>;
|
||||||
|
convertDragDataToVWBStep: (dragData: string, position: { x: number; y: number }) => Promise<Step | null>;
|
||||||
|
resolveStepType: (step: Step) => Promise<StepTypeResolutionResult>;
|
||||||
|
invalidateResolutionCache: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook pour l'intégration des étapes VWB dans le workflow
|
||||||
|
* Version 2.0 avec intégration StepTypeResolver
|
||||||
|
*/
|
||||||
|
export const useVWBStepIntegration = (): {
|
||||||
|
state: VWBStepIntegrationState;
|
||||||
|
methods: VWBStepIntegrationMethods;
|
||||||
|
} => {
|
||||||
|
const [vwbActions, setVwbActions] = useState<Map<string, VWBCatalogAction>>(new Map());
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [resolutionCache, setResolutionCache] = useState<Map<string, StepTypeResolutionResult>>(new Map());
|
||||||
|
const [lastCacheUpdate, setLastCacheUpdate] = useState(Date.now());
|
||||||
|
|
||||||
|
// Références pour optimisation
|
||||||
|
const loadingActionsRef = useRef<Set<string>>(new Set());
|
||||||
|
const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||||
|
|
||||||
|
// Créer une étape VWB à partir d'une action du catalogue
|
||||||
|
const createVWBStep = useCallback(async (
|
||||||
|
actionId: string,
|
||||||
|
position: { x: number; y: number }
|
||||||
|
): Promise<Step | null> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Charger les détails de l'action depuis le catalogue
|
||||||
|
const action = await catalogService.getActionDetails(actionId);
|
||||||
|
if (!action) {
|
||||||
|
throw new Error(`Action VWB non trouvée : ${actionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre en cache l'action
|
||||||
|
setVwbActions(prev => new Map(prev).set(actionId, action));
|
||||||
|
|
||||||
|
// Créer les paramètres par défaut
|
||||||
|
const defaultParameters: Record<string, any> = {};
|
||||||
|
Object.entries(action.parameters).forEach(([paramName, paramConfig]) => {
|
||||||
|
if (paramConfig.default !== undefined) {
|
||||||
|
defaultParameters[paramName] = paramConfig.default;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Créer les données de l'étape
|
||||||
|
const stepData: StepData = {
|
||||||
|
label: action.name,
|
||||||
|
stepType: actionId as StepType, // Utiliser l'ID de l'action comme type
|
||||||
|
parameters: defaultParameters,
|
||||||
|
isVWBCatalogAction: true,
|
||||||
|
vwbActionId: actionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Créer l'étape complète
|
||||||
|
const step: Step = {
|
||||||
|
id: `vwb_step_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
||||||
|
type: actionId as StepType,
|
||||||
|
name: action.name,
|
||||||
|
position,
|
||||||
|
data: stepData,
|
||||||
|
executionState: StepExecutionState.IDLE,
|
||||||
|
validationErrors: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return step;
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Erreur lors de la création de l\'étape VWB';
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error('Erreur lors de la création de l\'étape VWB:', err);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Vérifier si un type d'étape correspond à une action VWB (utilise StepTypeResolver)
|
||||||
|
const isVWBAction = useCallback((stepType: string): boolean => {
|
||||||
|
try {
|
||||||
|
// Créer une étape temporaire pour la détection
|
||||||
|
const tempStep: Step = {
|
||||||
|
id: 'temp',
|
||||||
|
type: stepType as StepType,
|
||||||
|
name: 'temp',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: {
|
||||||
|
label: 'temp',
|
||||||
|
stepType: stepType as StepType,
|
||||||
|
parameters: {}
|
||||||
|
},
|
||||||
|
executionState: StepExecutionState.IDLE,
|
||||||
|
validationErrors: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Utiliser le StepTypeResolver pour une détection robuste
|
||||||
|
return stepTypeResolver.isVWBAction(tempStep);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [useVWBStepIntegration] Erreur détection VWB:', error);
|
||||||
|
|
||||||
|
// Fallback vers l'ancienne logique
|
||||||
|
return vwbActions.has(stepType) ||
|
||||||
|
stepType.startsWith('vwb_') ||
|
||||||
|
stepType.includes('catalog_') ||
|
||||||
|
stepType.includes('_anchor') ||
|
||||||
|
stepType.includes('_text');
|
||||||
|
}
|
||||||
|
}, [vwbActions]);
|
||||||
|
|
||||||
|
// Obtenir une action VWB depuis le cache
|
||||||
|
const getVWBAction = useCallback((actionId: string): VWBCatalogAction | null => {
|
||||||
|
return vwbActions.get(actionId) || null;
|
||||||
|
}, [vwbActions]);
|
||||||
|
|
||||||
|
// Charger une action VWB - Priorité au catalogue statique (instantané)
|
||||||
|
const loadVWBAction = useCallback(async (actionId: string): Promise<VWBCatalogAction | null> => {
|
||||||
|
// Vérifier d'abord le cache mémoire
|
||||||
|
const cachedAction = vwbActions.get(actionId);
|
||||||
|
if (cachedAction) {
|
||||||
|
console.log('📦 [useVWBStepIntegration] Action trouvée en cache:', actionId);
|
||||||
|
return cachedAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Éviter les chargements multiples simultanés
|
||||||
|
if (loadingActionsRef.current.has(actionId)) {
|
||||||
|
// Attendre brièvement que le chargement en cours se termine (max 500ms)
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let attempts = 0;
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
attempts++;
|
||||||
|
if (!loadingActionsRef.current.has(actionId) || attempts > 10) {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
resolve(vwbActions.get(actionId) || null);
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingActionsRef.current.add(actionId);
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// PRIORITÉ 1: Catalogue statique (instantané, pas de réseau)
|
||||||
|
let staticAction = getStaticActionById(actionId);
|
||||||
|
|
||||||
|
// Si pas trouvé exactement, essayer avec recherche fuzzy
|
||||||
|
if (!staticAction) {
|
||||||
|
staticAction = findActionWithFallback(actionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (staticAction) {
|
||||||
|
setVwbActions(prev => new Map(prev).set(actionId, staticAction as VWBCatalogAction));
|
||||||
|
console.log('✅ [useVWBStepIntegration] Action VWB chargée (statique):', actionId, staticAction.name);
|
||||||
|
setError(null);
|
||||||
|
return staticAction as VWBCatalogAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRIORITÉ 2: Service catalogue (si statique n'a pas trouvé)
|
||||||
|
// Avec timeout court pour ne pas bloquer l'UI
|
||||||
|
try {
|
||||||
|
const timeoutPromise = new Promise<null>((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('Timeout')), 1500)
|
||||||
|
);
|
||||||
|
const servicePromise = catalogService.getActionDetails(actionId);
|
||||||
|
|
||||||
|
const action = await Promise.race([servicePromise, timeoutPromise]);
|
||||||
|
if (action) {
|
||||||
|
setVwbActions(prev => new Map(prev).set(actionId, action));
|
||||||
|
console.log('✅ [useVWBStepIntegration] Action VWB chargée (service):', actionId);
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
} catch (serviceErr) {
|
||||||
|
console.warn('⚠️ [useVWBStepIntegration] Service catalogue indisponible:', actionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('⚠️ [useVWBStepIntegration] Action VWB non trouvée:', actionId);
|
||||||
|
setError(`Action VWB non trouvée: ${actionId}`);
|
||||||
|
return null;
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Erreur de chargement';
|
||||||
|
console.error('❌ [useVWBStepIntegration] Erreur chargement action:', errorMessage);
|
||||||
|
setError(`Erreur chargement action ${actionId}: ${errorMessage}`);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
loadingActionsRef.current.delete(actionId);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [vwbActions]);
|
||||||
|
|
||||||
|
// Valider une étape VWB
|
||||||
|
const validateVWBStep = useCallback(async (step: Step): Promise<boolean> => {
|
||||||
|
if (!step.data.isVWBCatalogAction || !step.data.vwbActionId) {
|
||||||
|
return true; // Pas une étape VWB, validation OK
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await catalogService.validateAction({
|
||||||
|
type: step.data.vwbActionId,
|
||||||
|
parameters: step.data.parameters,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.is_valid;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur lors de la validation de l\'étape VWB:', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Résoudre le type d'une étape avec le StepTypeResolver
|
||||||
|
const resolveStepType = useCallback(async (step: Step): Promise<StepTypeResolutionResult> => {
|
||||||
|
const cacheKey = `${step.type}_${step.data?.vwbActionId || 'none'}`;
|
||||||
|
|
||||||
|
// Vérifier le cache local
|
||||||
|
const cached = resolutionCache.get(cacheKey);
|
||||||
|
if (cached && Date.now() - cached.timestamp < 30000) { // Cache 30 secondes
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await stepTypeResolver.resolveParameterConfig(step, {
|
||||||
|
enableCache: true,
|
||||||
|
enableLogging: process.env.NODE_ENV === 'development',
|
||||||
|
fallbackToEmpty: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mettre en cache le résultat
|
||||||
|
setResolutionCache(prev => new Map(prev).set(cacheKey, result));
|
||||||
|
setLastCacheUpdate(Date.now());
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [useVWBStepIntegration] Erreur résolution type:', error);
|
||||||
|
|
||||||
|
// Fallback basique
|
||||||
|
const fallbackResult: StepTypeResolutionResult = {
|
||||||
|
stepType: step.type as string,
|
||||||
|
isVWBAction: isVWBAction(step.type as string),
|
||||||
|
isStandardType: false,
|
||||||
|
parameterConfig: [],
|
||||||
|
detectionMethods: { fallback: true },
|
||||||
|
resolutionSource: 'fallback',
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
return fallbackResult;
|
||||||
|
}
|
||||||
|
}, [resolutionCache, isVWBAction]);
|
||||||
|
|
||||||
|
// Invalider le cache de résolution
|
||||||
|
const invalidateResolutionCache = useCallback(() => {
|
||||||
|
setResolutionCache(new Map());
|
||||||
|
setLastCacheUpdate(Date.now());
|
||||||
|
stepTypeResolver.invalidateCache();
|
||||||
|
console.log('🗑️ [useVWBStepIntegration] Cache de résolution invalidé');
|
||||||
|
}, []);
|
||||||
|
// Convertir les données de drag-and-drop en étape VWB
|
||||||
|
const convertDragDataToVWBStep = useCallback(async (
|
||||||
|
dragData: string,
|
||||||
|
position: { x: number; y: number }
|
||||||
|
): Promise<Step | null> => {
|
||||||
|
// Vérifier si c'est une action du catalogue
|
||||||
|
if (dragData.startsWith('catalog:')) {
|
||||||
|
const actionId = dragData.replace('catalog:', '');
|
||||||
|
return await createVWBStep(actionId, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si c'est une action VWB directe
|
||||||
|
if (isVWBAction(dragData)) {
|
||||||
|
return await createVWBStep(dragData, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // Pas une action VWB
|
||||||
|
}, [createVWBStep, isVWBAction]);
|
||||||
|
|
||||||
|
// État exposé (mémorisé pour éviter les re-renders inutiles)
|
||||||
|
const state: VWBStepIntegrationState = useMemo(() => ({
|
||||||
|
vwbActions,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
resolutionCache,
|
||||||
|
lastCacheUpdate,
|
||||||
|
}), [vwbActions, isLoading, error, resolutionCache, lastCacheUpdate]);
|
||||||
|
|
||||||
|
// Méthodes exposées (mémorisées pour éviter les boucles infinies dans les useEffect)
|
||||||
|
const methods: VWBStepIntegrationMethods = useMemo(() => ({
|
||||||
|
createVWBStep,
|
||||||
|
isVWBAction,
|
||||||
|
getVWBAction,
|
||||||
|
loadVWBAction,
|
||||||
|
validateVWBStep,
|
||||||
|
convertDragDataToVWBStep,
|
||||||
|
resolveStepType,
|
||||||
|
invalidateResolutionCache,
|
||||||
|
}), [
|
||||||
|
createVWBStep,
|
||||||
|
isVWBAction,
|
||||||
|
getVWBAction,
|
||||||
|
loadVWBAction,
|
||||||
|
validateVWBStep,
|
||||||
|
convertDragDataToVWBStep,
|
||||||
|
resolveStepType,
|
||||||
|
invalidateResolutionCache,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { state, methods };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Liste des actions VWB connues pour détection basée sur le type
|
||||||
|
const KNOWN_VWB_ACTIONS = [
|
||||||
|
// Vision UI
|
||||||
|
'click_anchor', 'double_click_anchor', 'right_click_anchor',
|
||||||
|
'hover_anchor', 'drag_drop_anchor',
|
||||||
|
'type_text', 'type_secret', 'wait_for_anchor',
|
||||||
|
'extract_text', 'screenshot_evidence', 'scroll_to_anchor',
|
||||||
|
'focus_anchor', 'hotkey', 'keyboard_shortcut',
|
||||||
|
// Navigation
|
||||||
|
'navigate_to_url', 'browser_back',
|
||||||
|
// Validation
|
||||||
|
'verify_element_exists', 'verify_text_content',
|
||||||
|
// Intelligence
|
||||||
|
'ai_analyze_text',
|
||||||
|
// Data
|
||||||
|
'extract_table', 'download_to_folder',
|
||||||
|
// Database
|
||||||
|
'db_save_data', 'db_read_data',
|
||||||
|
// Control
|
||||||
|
'visual_condition', 'loop_visual'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fonction utilitaire pour détecter si un type est une action VWB
|
||||||
|
*/
|
||||||
|
const isVWBActionType = (stepType: string): boolean => {
|
||||||
|
if (KNOWN_VWB_ACTIONS.includes(stepType)) return true;
|
||||||
|
if (stepType.includes('_anchor')) return true;
|
||||||
|
if (stepType.includes('_text') && stepType !== 'type') return true;
|
||||||
|
if (stepType.includes('_secret')) return true;
|
||||||
|
if (stepType.startsWith('vwb_')) return true;
|
||||||
|
if (stepType.includes('catalog_')) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook utilitaire pour vérifier si une étape est une action VWB
|
||||||
|
*/
|
||||||
|
export const useIsVWBStep = (step: Step | null): boolean => {
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!step) return false;
|
||||||
|
return Boolean(
|
||||||
|
step.data.isVWBCatalogAction ||
|
||||||
|
step.data.vwbActionId ||
|
||||||
|
isVWBActionType(step.type as string)
|
||||||
|
);
|
||||||
|
}, [step]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook utilitaire pour obtenir l'ID de l'action VWB d'une étape
|
||||||
|
* Amélioration: détecte aussi les actions VWB basées sur le type même sans flag
|
||||||
|
*/
|
||||||
|
export const useVWBActionId = (step: Step | null): string | null => {
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!step) return null;
|
||||||
|
|
||||||
|
// Si le flag est explicitement défini, l'utiliser
|
||||||
|
if (step.data.isVWBCatalogAction) {
|
||||||
|
return step.data.vwbActionId || step.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si vwbActionId est défini, l'utiliser
|
||||||
|
if (step.data.vwbActionId) {
|
||||||
|
return step.data.vwbActionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: détecter basé sur le type d'étape
|
||||||
|
if (isVWBActionType(step.type as string)) {
|
||||||
|
console.log('🔍 [useVWBActionId] Détection VWB par type:', step.type);
|
||||||
|
return step.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [step]);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user