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:
Dom
2026-01-14 23:40:55 +01:00
parent 48a883d1e7
commit 360c336608

View File

@@ -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]);
};