diff --git a/visual_workflow_builder/backend/actions/validation/verify_element_exists.py b/visual_workflow_builder/backend/actions/validation/verify_element_exists.py new file mode 100644 index 000000000..fb0831b2d --- /dev/null +++ b/visual_workflow_builder/backend/actions/validation/verify_element_exists.py @@ -0,0 +1,296 @@ +""" +Action Validation Existence - Vérifier qu'un élément visuel existe +Auteur : Dom, Alice, Kiro, Claude - 14 janvier 2026 + +Cette action vérifie l'existence d'un élément visuel sur l'écran +en utilisant le template matching OpenCV. +""" + +from typing import Dict, Any, List, Optional +from datetime import datetime +import time + +# Import des modules de base +from ..base_action import BaseVWBAction, VWBActionResult, VWBActionStatus +from ...contracts.error import VWBErrorType, create_vwb_error +from ...contracts.evidence import VWBEvidenceType, create_screenshot_evidence +from ...contracts.visual_anchor import VWBVisualAnchor + + +class VWBVerifyElementExistsAction(BaseVWBAction): + """ + Action pour vérifier l'existence d'un élément visuel. + + Cette action utilise le template matching OpenCV pour rechercher + un élément sur l'écran et valider s'il est présent ou absent. + """ + + def __init__( + self, + action_id: str, + parameters: Dict[str, Any], + screen_capturer=None + ): + """ + Initialise l'action de vérification d'existence. + + Args: + action_id: Identifiant unique de l'action + parameters: Paramètres incluant l'ancre visuelle + screen_capturer: Instance du ScreenCapturer (Option A thread-safe) + """ + super().__init__( + action_id=action_id, + name="Vérifier Existence Élément", + description="Vérifie qu'un élément visuel existe ou n'existe pas sur l'écran", + parameters=parameters, + screen_capturer=screen_capturer + ) + + # Paramètres spécifiques à la vérification + self.visual_anchor: Optional[VWBVisualAnchor] = parameters.get('visual_anchor') + self.should_exist = parameters.get('should_exist', True) + + # Configuration de matching + self.confidence_threshold = parameters.get('confidence_threshold', 0.7) + self.search_timeout_ms = parameters.get('search_timeout_ms', 5000) + + def validate_parameters(self) -> List[str]: + """Valide les paramètres de l'action.""" + errors = [] + + # Vérifier l'ancre visuelle + if not self.visual_anchor: + errors.append("Ancre visuelle requise") + elif isinstance(self.visual_anchor, dict): + # Si c'est un dict, vérifier qu'il a les champs nécessaires + if not self.visual_anchor.get('screenshot') and not self.visual_anchor.get('image_base64'): + errors.append("Ancre visuelle doit contenir une image (screenshot ou image_base64)") + elif isinstance(self.visual_anchor, VWBVisualAnchor): + if not self.visual_anchor.is_active: + errors.append("Ancre visuelle inactive") + + # Vérifier le seuil de confiance + if not (0.0 <= self.confidence_threshold <= 1.0): + errors.append("Seuil de confiance doit être entre 0.0 et 1.0") + + return errors + + def execute_core(self, step_id: str) -> VWBActionResult: + """ + Exécute la vérification d'existence de l'élément. + + Args: + step_id: Identifiant de l'étape + + Returns: + Résultat d'exécution + """ + start_time = datetime.now() + + try: + # Importer la fonction de recherche visuelle + from ...catalog_routes import find_visual_anchor_on_screen + + # Obtenir l'image de l'ancre + anchor_image = self._get_anchor_image() + if not anchor_image: + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.PARAMETER_INVALID, + message="Impossible d'obtenir l'image de l'ancre visuelle" + ) + + # Obtenir le bounding_box si disponible + bounding_box = self._get_bounding_box() + + # Rechercher l'élément sur l'écran + anchor_name = self._get_anchor_name() + print(f"🔍 Recherche de l'élément: {anchor_name}") + + search_result = find_visual_anchor_on_screen( + anchor_image_base64=anchor_image, + confidence_threshold=self.confidence_threshold, + bounding_box=bounding_box + ) + + # Analyser le résultat + element_found = search_result is not None and search_result.get('found', False) + confidence = search_result.get('confidence', 0.0) if search_result else 0.0 + + # Vérifier si le résultat correspond à l'attente + verification_success = (element_found == self.should_exist) + + end_time = datetime.now() + execution_time = (end_time - start_time).total_seconds() * 1000 + + # Créer le résultat + if verification_success: + status_msg = "présent" if element_found else "absent" + print(f"✅ Vérification réussie: élément {status_msg} (confiance: {confidence:.2%})") + + result = VWBActionResult( + action_id=self.action_id, + step_id=step_id, + status=VWBActionStatus.SUCCESS, + start_time=start_time, + end_time=end_time, + execution_time_ms=execution_time, + output_data={ + 'element_found': element_found, + 'should_exist': self.should_exist, + 'verification_success': True, + 'confidence': confidence, + 'match_position': search_result if element_found else None + }, + evidence_list=self.evidence_list.copy() + ) + + return result + else: + expected = "présent" if self.should_exist else "absent" + actual = "présent" if element_found else "absent" + error_msg = f"Élément attendu {expected} mais trouvé {actual}" + print(f"❌ Vérification échouée: {error_msg}") + + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.ELEMENT_NOT_FOUND if self.should_exist else VWBErrorType.VALIDATION_FAILED, + message=error_msg, + technical_details={ + 'element_found': element_found, + 'should_exist': self.should_exist, + 'confidence': confidence, + 'match_position': search_result if element_found else None + } + ) + + except ImportError as e: + print(f"⚠️ Erreur import: {e}") + # Fallback si catalog_routes n'est pas disponible + return self._execute_fallback(step_id, start_time) + + except Exception as e: + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.SYSTEM_ERROR, + message=f"Erreur lors de la vérification: {str(e)}", + technical_details={'exception': str(e)} + ) + + def _get_anchor_image(self) -> Optional[str]: + """Récupère l'image base64 de l'ancre.""" + if isinstance(self.visual_anchor, dict): + # Format dictionnaire + return self.visual_anchor.get('screenshot') or self.visual_anchor.get('image_base64') + elif isinstance(self.visual_anchor, VWBVisualAnchor): + # Format objet VWBVisualAnchor + return self.visual_anchor.screenshot_base64 + return None + + def _get_bounding_box(self) -> Optional[Dict]: + """Récupère le bounding_box de l'ancre si disponible.""" + if isinstance(self.visual_anchor, dict): + return self.visual_anchor.get('bounding_box') + elif isinstance(self.visual_anchor, VWBVisualAnchor): + if self.visual_anchor.has_bounding_box(): + return self.visual_anchor.bounding_box + return None + + def _get_anchor_name(self) -> str: + """Récupère le nom de l'ancre.""" + if isinstance(self.visual_anchor, dict): + return self.visual_anchor.get('name', self.visual_anchor.get('anchor_id', 'Unknown')) + elif isinstance(self.visual_anchor, VWBVisualAnchor): + return self.visual_anchor.name + return "Unknown" + + def _execute_fallback(self, step_id: str, start_time: datetime) -> VWBActionResult: + """ + Fallback si la recherche visuelle n'est pas disponible. + Utilise pyautogui.locateOnScreen si disponible. + """ + try: + import pyautogui + from PIL import Image + from io import BytesIO + import base64 + + anchor_image = self._get_anchor_image() + if not anchor_image: + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.PARAMETER_INVALID, + message="Image de l'ancre non disponible" + ) + + # Décoder l'image + if ',' in anchor_image: + anchor_image = anchor_image.split(',')[1] + + image_bytes = base64.b64decode(anchor_image) + pil_image = Image.open(BytesIO(image_bytes)) + + # Rechercher avec pyautogui + location = pyautogui.locateOnScreen(pil_image, confidence=self.confidence_threshold) + + element_found = location is not None + verification_success = (element_found == self.should_exist) + + end_time = datetime.now() + execution_time = (end_time - start_time).total_seconds() * 1000 + + if verification_success: + return VWBActionResult( + action_id=self.action_id, + step_id=step_id, + status=VWBActionStatus.SUCCESS, + start_time=start_time, + end_time=end_time, + execution_time_ms=execution_time, + output_data={ + 'element_found': element_found, + 'should_exist': self.should_exist, + 'verification_success': True, + 'method': 'pyautogui_fallback' + }, + evidence_list=self.evidence_list.copy() + ) + else: + expected = "présent" if self.should_exist else "absent" + actual = "présent" if element_found else "absent" + + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.VALIDATION_FAILED, + message=f"Élément attendu {expected} mais trouvé {actual}", + technical_details={'method': 'pyautogui_fallback'} + ) + + except Exception as e: + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.SYSTEM_ERROR, + message=f"Erreur fallback: {str(e)}" + ) + + def get_action_info(self) -> Dict[str, Any]: + """Retourne les informations de l'action pour l'interface.""" + return { + 'action_id': self.action_id, + 'name': self.name, + 'description': self.description, + 'type': 'verify_element_exists', + 'parameters': { + 'anchor_name': self._get_anchor_name(), + 'should_exist': self.should_exist, + 'confidence_threshold': self.confidence_threshold + }, + 'status': self.current_status.value + } diff --git a/visual_workflow_builder/backend/actions/validation/verify_text_content.py b/visual_workflow_builder/backend/actions/validation/verify_text_content.py new file mode 100644 index 000000000..dd7c45143 --- /dev/null +++ b/visual_workflow_builder/backend/actions/validation/verify_text_content.py @@ -0,0 +1,391 @@ +""" +Action Validation Texte - Vérifier le contenu textuel d'un élément +Auteur : Dom, Alice, Kiro, Claude - 14 janvier 2026 + +Cette action vérifie qu'un texte spécifique est présent dans une zone +de l'écran, en utilisant l'OCR pour extraire et comparer le texte. + +Modes OCR disponibles: +- ollama: Utilise un modèle de vision local (GPU, meilleure qualité) +- easyocr: OCR traditionnel (CPU/GPU, plus rapide) +""" + +from typing import Dict, Any, List, Optional +from datetime import datetime +import time +import re +import base64 +from io import BytesIO + +# Import des modules de base +from ..base_action import BaseVWBAction, VWBActionResult, VWBActionStatus +from ...contracts.error import VWBErrorType, create_vwb_error +from ...contracts.evidence import VWBEvidenceType, create_screenshot_evidence +from ...contracts.visual_anchor import VWBVisualAnchor + + +class VWBVerifyTextContentAction(BaseVWBAction): + """ + Action pour vérifier le contenu textuel d'un élément. + + Cette action extrait le texte d'une zone de l'écran via OCR + et vérifie qu'il correspond au texte attendu. + + Supporte deux modes OCR: + - ollama: Modèle de vision local (meilleure qualité, utilise GPU) + - easyocr: OCR traditionnel (plus rapide, fallback) + """ + + # Configuration Ollama par défaut + OLLAMA_URL = "http://localhost:11434" + OLLAMA_MODEL = "qwen2.5-vl:7b" # Modèle de vision Qwen - excellent pour OCR + + def __init__( + self, + action_id: str, + parameters: Dict[str, Any], + screen_capturer=None + ): + """ + Initialise l'action de vérification de texte. + + Args: + action_id: Identifiant unique de l'action + parameters: Paramètres incluant l'ancre visuelle et le texte attendu + screen_capturer: Instance du ScreenCapturer (Option A thread-safe) + """ + super().__init__( + action_id=action_id, + name="Vérifier Contenu Texte", + description="Vérifie qu'un texte spécifique est présent dans une zone de l'écran", + parameters=parameters, + screen_capturer=screen_capturer + ) + + # Paramètres spécifiques à la vérification + self.visual_anchor: Optional[VWBVisualAnchor] = parameters.get('visual_anchor') + self.expected_text = parameters.get('expected_text', '') + self.match_mode = parameters.get('match_mode', 'contains') # exact, contains, regex + self.case_sensitive = parameters.get('case_sensitive', False) + + # Configuration OCR + self.ocr_mode = parameters.get('ocr_mode', 'ollama') # ollama (GPU) ou easyocr + self.ollama_model = parameters.get('ollama_model', self.OLLAMA_MODEL) + self.ollama_url = parameters.get('ollama_url', self.OLLAMA_URL) + + # Configuration de matching + self.confidence_threshold = parameters.get('confidence_threshold', 0.7) + + def validate_parameters(self) -> List[str]: + """Valide les paramètres de l'action.""" + errors = [] + + # Vérifier le texte attendu + if not self.expected_text: + errors.append("Texte attendu requis") + + # Vérifier l'ancre visuelle (optionnelle pour recherche plein écran) + if self.visual_anchor: + if isinstance(self.visual_anchor, dict): + if not self.visual_anchor.get('screenshot') and not self.visual_anchor.get('image_base64'): + if not self.visual_anchor.get('bounding_box'): + errors.append("Ancre visuelle doit contenir une image ou un bounding_box") + elif isinstance(self.visual_anchor, VWBVisualAnchor): + if not self.visual_anchor.is_active: + errors.append("Ancre visuelle inactive") + + # Vérifier le mode de matching + if self.match_mode not in ['exact', 'contains', 'regex', 'starts_with', 'ends_with']: + errors.append(f"Mode de matching invalide: {self.match_mode}") + + # Vérifier le mode OCR + if self.ocr_mode not in ['ollama', 'easyocr']: + errors.append(f"Mode OCR invalide: {self.ocr_mode} (utilisez 'ollama' ou 'easyocr')") + + return errors + + def execute_core(self, step_id: str) -> VWBActionResult: + """ + Exécute la vérification du contenu textuel. + + Args: + step_id: Identifiant de l'étape + + Returns: + Résultat d'exécution + """ + start_time = datetime.now() + + try: + anchor_name = self._get_anchor_name() + print(f"🔍 Vérification texte: '{self.expected_text}' dans {anchor_name}") + print(f" Mode OCR: {self.ocr_mode}") + + # Extraire le texte de la zone + extracted_text = self._extract_text_from_screen() + + if extracted_text is None: + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.SCREEN_CAPTURE_FAILED, + message="Impossible d'extraire le texte de l'écran" + ) + + # Comparer le texte + text_matches = self._compare_text(extracted_text, self.expected_text) + + end_time = datetime.now() + execution_time = (end_time - start_time).total_seconds() * 1000 + + if text_matches: + print(f"✅ Texte trouvé: '{self.expected_text}'") + + return VWBActionResult( + action_id=self.action_id, + step_id=step_id, + status=VWBActionStatus.SUCCESS, + start_time=start_time, + end_time=end_time, + execution_time_ms=execution_time, + output_data={ + 'text_matches': True, + 'expected_text': self.expected_text, + 'extracted_text': extracted_text, + 'match_mode': self.match_mode, + 'ocr_mode': self.ocr_mode + }, + evidence_list=self.evidence_list.copy() + ) + else: + error_msg = f"Texte attendu '{self.expected_text}' non trouvé" + print(f"❌ {error_msg}") + if extracted_text: + print(f" Texte extrait: '{extracted_text[:100]}...'") + + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.VALIDATION_FAILED, + message=error_msg, + technical_details={ + 'expected_text': self.expected_text, + 'extracted_text': extracted_text[:500] if extracted_text else '', + 'match_mode': self.match_mode, + 'ocr_mode': self.ocr_mode + } + ) + + except Exception as e: + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.SYSTEM_ERROR, + message=f"Erreur lors de la vérification: {str(e)}", + technical_details={'exception': str(e)} + ) + + def _extract_text_from_screen(self) -> Optional[str]: + """ + Extrait le texte de l'écran ou d'une zone spécifique. + + Returns: + Texte extrait ou None si erreur + """ + try: + import pyautogui + from PIL import Image + + # Capturer l'écran + screenshot = pyautogui.screenshot() + + # Si on a un bounding_box, extraire la région + if self.visual_anchor: + bbox = self._get_bounding_box() + if bbox: + x = int(bbox.get('x', 0)) + y = int(bbox.get('y', 0)) + w = int(bbox.get('width', 100)) + h = int(bbox.get('height', 100)) + screenshot = screenshot.crop((x, y, x + w, y + h)) + + # Choisir le mode OCR + if self.ocr_mode == 'ollama': + return self._extract_with_ollama(screenshot) + else: + return self._extract_with_easyocr(screenshot) + + except Exception as e: + print(f"❌ Erreur extraction texte: {e}") + return None + + def _extract_with_ollama(self, image) -> Optional[str]: + """ + Extrait le texte en utilisant Ollama avec un modèle de vision. + + Args: + image: Image PIL à analyser + + Returns: + Texte extrait ou None si erreur + """ + try: + import requests + from PIL import Image + + print(f"🤖 Extraction OCR via Ollama ({self.ollama_model})...") + + # Convertir l'image en base64 + buffer = BytesIO() + image.save(buffer, format='PNG') + image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') + + # Appel à l'API Ollama + response = requests.post( + f"{self.ollama_url}/api/generate", + json={ + "model": self.ollama_model, + "prompt": "Extract and return ONLY the text visible in this image. Do not add any explanation or commentary. Just return the raw text content.", + "images": [image_base64], + "stream": False, + "options": { + "temperature": 0.1, # Réponse plus déterministe + "num_predict": 500 # Limite de tokens + } + }, + timeout=30 + ) + + if response.status_code == 200: + result = response.json() + extracted_text = result.get('response', '').strip() + print(f" ✅ Texte extrait ({len(extracted_text)} caractères)") + return extracted_text + else: + print(f" ⚠️ Erreur Ollama: {response.status_code}") + # Fallback sur easyocr + return self._extract_with_easyocr(image) + + except requests.exceptions.ConnectionError: + print(f" ⚠️ Ollama non disponible, fallback sur easyocr") + return self._extract_with_easyocr(image) + + except Exception as e: + print(f" ⚠️ Erreur Ollama: {e}, fallback sur easyocr") + return self._extract_with_easyocr(image) + + def _extract_with_easyocr(self, image) -> Optional[str]: + """ + Extrait le texte en utilisant EasyOCR. + + Args: + image: Image PIL à analyser + + Returns: + Texte extrait ou None si erreur + """ + try: + import easyocr + import numpy as np + + print("📝 Extraction OCR via EasyOCR...") + + # Convertir en array numpy + img_array = np.array(image) + + # EasyOCR (GPU si disponible) + reader = easyocr.Reader(['fr', 'en'], gpu=True) + results = reader.readtext(img_array) + + # Combiner les résultats + extracted_text = ' '.join([result[1] for result in results]) + print(f" ✅ Texte extrait ({len(extracted_text)} caractères)") + return extracted_text.strip() + + except ImportError: + print(" ⚠️ easyocr non disponible") + return "" + + except Exception as e: + print(f" ❌ Erreur EasyOCR: {e}") + return None + + def _compare_text(self, extracted: str, expected: str) -> bool: + """ + Compare le texte extrait avec le texte attendu. + + Args: + extracted: Texte extrait de l'écran + expected: Texte attendu + + Returns: + True si le texte correspond selon le mode de matching + """ + if not extracted: + return False + + # Normaliser si non sensible à la casse + if not self.case_sensitive: + extracted = extracted.lower() + expected = expected.lower() + + # Appliquer le mode de matching + if self.match_mode == 'exact': + return extracted.strip() == expected.strip() + + elif self.match_mode == 'contains': + return expected in extracted + + elif self.match_mode == 'starts_with': + return extracted.strip().startswith(expected) + + elif self.match_mode == 'ends_with': + return extracted.strip().endswith(expected) + + elif self.match_mode == 'regex': + try: + flags = 0 if self.case_sensitive else re.IGNORECASE + pattern = re.compile(expected, flags) + return bool(pattern.search(extracted)) + except re.error: + print(f" ⚠️ Pattern regex invalide: {expected}") + return False + + return False + + def _get_bounding_box(self) -> Optional[Dict]: + """Récupère le bounding_box de l'ancre si disponible.""" + if isinstance(self.visual_anchor, dict): + return self.visual_anchor.get('bounding_box') + elif isinstance(self.visual_anchor, VWBVisualAnchor): + if self.visual_anchor.has_bounding_box(): + return self.visual_anchor.bounding_box + return None + + def _get_anchor_name(self) -> str: + """Récupère le nom de l'ancre.""" + if not self.visual_anchor: + return "écran entier" + if isinstance(self.visual_anchor, dict): + return self.visual_anchor.get('name', self.visual_anchor.get('anchor_id', 'zone')) + elif isinstance(self.visual_anchor, VWBVisualAnchor): + return self.visual_anchor.name + return "zone" + + def get_action_info(self) -> Dict[str, Any]: + """Retourne les informations de l'action pour l'interface.""" + return { + 'action_id': self.action_id, + 'name': self.name, + 'description': self.description, + 'type': 'verify_text_content', + 'parameters': { + 'anchor_name': self._get_anchor_name(), + 'expected_text': self.expected_text, + 'match_mode': self.match_mode, + 'case_sensitive': self.case_sensitive, + 'ocr_mode': self.ocr_mode, + 'ollama_model': self.ollama_model + }, + 'status': self.current_status.value + } diff --git a/visual_workflow_builder/frontend/src/data/staticCatalog.ts b/visual_workflow_builder/frontend/src/data/staticCatalog.ts new file mode 100644 index 000000000..d30f6ef27 --- /dev/null +++ b/visual_workflow_builder/frontend/src/data/staticCatalog.ts @@ -0,0 +1,1434 @@ +/** + * Catalogue Statique d'Actions VisionOnly 100% Visuel + * Auteur : Dom, Alice, Kiro - 13 janvier 2026 + * + * Ce catalogue contient UNIQUEMENT des actions basées sur la vision. + * Toutes les actions utilisent des ancres visuelles pour l'identification des éléments. + * + * Version 3.0 - Catalogue 100% visuel avec IA et base de données + */ + +import { VWBCatalogAction, VWBActionCategory } from '../types/catalog'; + +/** + * Interface pour les métadonnées de fallback + */ +export interface FallbackMetadata { + isFallback: boolean; + fallbackReason: string; + originalActionId: string; + createdAt: number; + confidence: number; +} + +/** + * Action avec métadonnées de fallback + */ +export interface StaticCatalogAction extends VWBCatalogAction { + fallbackMetadata?: FallbackMetadata; +} + +/** + * Actions VisionOnly 100% Visuelles + */ +export const STATIC_CATALOG_ACTIONS: StaticCatalogAction[] = [ + // ═══════════════════════════════════════════════════════════════════════════ + // CATÉGORIE: INTERACTIONS VISUELLES (vision_ui) + // ═══════════════════════════════════════════════════════════════════════════ + { + id: 'click_anchor', + name: 'Cliquer sur élément', + category: 'vision_ui' as VWBActionCategory, + description: 'Cliquer sur un élément identifié visuellement', + icon: '🖱️', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Élément visuel à cliquer', + default: null, + }, + timeout_ms: { + type: 'number', + required: false, + description: 'Timeout en millisecondes', + default: 5000, + min: 1000, + max: 30000, + }, + }, + examples: [ + { + name: 'Cliquer sur bouton Valider', + description: 'Clic simple sur un bouton de validation', + parameters: { timeout_ms: 5000 }, + expectedResult: 'Le bouton est cliqué', + }, + ], + documentation: 'Effectue un clic simple sur un élément identifié par reconnaissance visuelle.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['ui', 'click', 'interaction', 'vision'], + complexity: 'simple', + estimatedDuration: 2000, + }, + }, + { + id: 'double_click_anchor', + name: 'Double-cliquer sur élément', + category: 'vision_ui' as VWBActionCategory, + description: 'Double-cliquer sur un élément identifié visuellement', + icon: '🖱️🖱️', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Élément visuel à double-cliquer', + default: null, + }, + click_interval_ms: { + type: 'number', + required: false, + description: 'Intervalle entre les deux clics (ms)', + default: 100, + min: 50, + max: 500, + }, + }, + examples: [ + { + name: 'Ouvrir un fichier', + description: 'Double-clic pour ouvrir un fichier ou dossier', + parameters: { click_interval_ms: 100 }, + expectedResult: 'L\'élément est ouvert', + }, + ], + documentation: 'Effectue un double-clic sur un élément identifié visuellement. Utile pour ouvrir des fichiers, éditer des cellules, etc.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['ui', 'double-click', 'interaction', 'vision'], + complexity: 'simple', + estimatedDuration: 2000, + }, + }, + { + id: 'right_click_anchor', + name: 'Clic droit sur élément', + category: 'vision_ui' as VWBActionCategory, + description: 'Effectuer un clic droit pour ouvrir un menu contextuel', + icon: '📋', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Élément visuel pour le clic droit', + default: null, + }, + }, + examples: [ + { + name: 'Menu contextuel fichier', + description: 'Clic droit sur un fichier pour afficher les options', + parameters: {}, + expectedResult: 'Le menu contextuel s\'affiche', + }, + ], + documentation: 'Effectue un clic droit sur un élément pour afficher son menu contextuel.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['ui', 'right-click', 'context-menu', 'vision'], + complexity: 'simple', + estimatedDuration: 1500, + }, + }, + { + id: 'hover_anchor', + name: 'Survoler élément', + category: 'vision_ui' as VWBActionCategory, + description: 'Survoler un élément pour afficher un tooltip ou déclencher un effet', + icon: '👆', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Élément visuel à survoler', + default: null, + }, + hover_duration_ms: { + type: 'number', + required: false, + description: 'Durée du survol en millisecondes', + default: 1000, + min: 500, + max: 10000, + }, + }, + examples: [ + { + name: 'Afficher tooltip', + description: 'Survoler un bouton pour afficher son tooltip', + parameters: { hover_duration_ms: 1500 }, + expectedResult: 'Le tooltip s\'affiche', + }, + ], + documentation: 'Survole un élément pour déclencher des effets hover (tooltips, menus déroulants, etc.).', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['ui', 'hover', 'tooltip', 'vision'], + complexity: 'simple', + estimatedDuration: 2000, + }, + }, + { + id: 'type_text', + name: 'Saisir texte', + category: 'vision_ui' as VWBActionCategory, + description: 'Saisir du texte dans un champ identifié visuellement', + icon: '⌨️', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Champ de saisie visuel cible', + default: null, + }, + text: { + type: 'string', + required: true, + description: 'Texte à saisir', + default: '', + }, + clear_before: { + type: 'boolean', + required: false, + description: 'Effacer le contenu avant la saisie', + default: true, + }, + }, + examples: [ + { + name: 'Saisir un email', + description: 'Saisir une adresse email dans un champ de formulaire', + parameters: { text: 'exemple@email.com', clear_before: true }, + expectedResult: 'Le texte est saisi dans le champ', + }, + ], + documentation: 'Saisit du texte dans un champ identifié visuellement avec option d\'effacement préalable.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['ui', 'text', 'input', 'vision'], + complexity: 'simple', + estimatedDuration: 1500, + }, + }, + { + id: 'type_secret', + name: 'Saisir secret', + category: 'vision_ui' as VWBActionCategory, + description: 'Saisir un texte sensible (mot de passe) de manière sécurisée', + icon: '🔐', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Champ de saisie secret cible', + default: null, + }, + secret_key: { + type: 'string', + required: true, + description: 'Clé du secret dans le gestionnaire sécurisé', + default: '', + }, + }, + examples: [ + { + name: 'Saisir mot de passe', + description: 'Saisir un mot de passe stocké de manière sécurisée', + parameters: { secret_key: 'mon_mdp_app' }, + expectedResult: 'Le mot de passe est saisi sans exposition', + }, + ], + documentation: 'Saisit un texte sensible depuis un gestionnaire de secrets sécurisé.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['ui', 'secret', 'password', 'security', 'vision'], + complexity: 'simple', + estimatedDuration: 1500, + }, + }, + { + id: 'focus_anchor', + name: 'Focaliser élément', + category: 'vision_ui' as VWBActionCategory, + description: 'Donner le focus à un élément identifié visuellement', + icon: '🎯', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Élément à focaliser', + default: null, + }, + }, + examples: [], + documentation: 'Donne le focus à un élément UI sans cliquer dessus.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['ui', 'focus', 'vision'], + complexity: 'simple', + estimatedDuration: 1000, + }, + }, + { + id: 'drag_drop_anchor', + name: 'Glisser-déposer', + category: 'vision_ui' as VWBActionCategory, + description: 'Glisser un élément et le déposer sur un autre', + icon: '↔️', + parameters: { + source_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Élément source à glisser', + default: null, + }, + target_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Élément cible où déposer', + default: null, + }, + drag_duration_ms: { + type: 'number', + required: false, + description: 'Durée du glissement en millisecondes', + default: 500, + min: 200, + max: 3000, + }, + }, + examples: [ + { + name: 'Déplacer fichier dans dossier', + description: 'Glisser un fichier vers un dossier', + parameters: { drag_duration_ms: 500 }, + expectedResult: 'Le fichier est déplacé dans le dossier cible', + }, + ], + documentation: 'Effectue un glisser-déposer entre deux éléments visuels.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['ui', 'drag', 'drop', 'interaction', 'vision'], + complexity: 'intermediate', + estimatedDuration: 3000, + }, + }, + + // ═══════════════════════════════════════════════════════════════════════════ + // CATÉGORIE: CONTRÔLE DE FLUX (control) + // ═══════════════════════════════════════════════════════════════════════════ + { + id: 'wait_for_anchor', + name: 'Attendre élément', + category: 'control' as VWBActionCategory, + description: 'Attendre qu\'un élément visuel apparaisse ou disparaisse', + icon: '⏳', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Élément visuel à attendre', + default: null, + }, + wait_for: { + type: 'string', + required: false, + description: 'Attendre apparition ou disparition', + default: 'appear', + options: ['appear', 'disappear'], + }, + timeout_ms: { + type: 'number', + required: false, + description: 'Timeout maximum en millisecondes', + default: 30000, + min: 1000, + max: 120000, + }, + }, + examples: [ + { + name: 'Attendre chargement', + description: 'Attendre qu\'un spinner de chargement disparaisse', + parameters: { wait_for: 'disappear', timeout_ms: 30000 }, + expectedResult: 'Le workflow continue quand l\'élément disparaît', + }, + ], + documentation: 'Synchronise le workflow avec l\'apparition ou la disparition d\'un élément visuel.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['control', 'wait', 'synchronization', 'vision'], + complexity: 'intermediate', + estimatedDuration: 5000, + }, + }, + { + id: 'keyboard_shortcut', + name: 'Raccourci clavier', + category: 'control' as VWBActionCategory, + description: 'Appuyer sur une touche ou combinaison de touches', + icon: '🎹', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: false, + description: 'Élément sur lequel mettre le focus avant d\'appuyer (optionnel)', + default: null, + }, + key: { + type: 'string', + required: true, + description: 'Touche ou combinaison à appuyer', + default: 'Enter', + options: [ + 'Enter', 'Tab', 'Escape', 'Space', 'Backspace', 'Delete', + 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', + 'Home', 'End', 'PageUp', 'PageDown', + 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12' + ], + }, + modifiers: { + type: 'string', + required: false, + description: 'Modificateurs (Ctrl, Alt, Shift, Meta)', + default: '', + options: ['', 'Ctrl', 'Alt', 'Shift', 'Ctrl+Shift', 'Ctrl+Alt', 'Alt+Shift', 'Ctrl+Alt+Shift', 'Meta'], + }, + custom_combination: { + type: 'string', + required: false, + description: 'Combinaison personnalisée (ex: Ctrl+S, Alt+F4)', + default: '', + }, + repeat_count: { + type: 'number', + required: false, + description: 'Nombre de fois à répéter la touche', + default: 1, + min: 1, + max: 10, + }, + delay_between_ms: { + type: 'number', + required: false, + description: 'Délai entre les répétitions (ms)', + default: 100, + min: 50, + max: 1000, + }, + }, + examples: [ + { + name: 'Valider un formulaire', + description: 'Appuyer sur Entrée pour valider', + parameters: { key: 'Enter' }, + expectedResult: 'La touche Entrée est pressée', + }, + { + name: 'Sauvegarder un document', + description: 'Ctrl+S pour sauvegarder', + parameters: { key: 'S', modifiers: 'Ctrl' }, + expectedResult: 'Le document est sauvegardé', + }, + { + name: 'Naviguer avec Tab', + description: 'Appuyer 3 fois sur Tab', + parameters: { key: 'Tab', repeat_count: 3, delay_between_ms: 200 }, + expectedResult: 'Le focus se déplace de 3 éléments', + }, + { + name: 'Fermer une fenêtre', + description: 'Alt+F4 pour fermer', + parameters: { custom_combination: 'Alt+F4' }, + expectedResult: 'La fenêtre se ferme', + }, + ], + documentation: 'Permet d\'appuyer sur des touches du clavier, avec ou sans focus sur un élément visuel. Supporte les touches simples, les combinaisons avec modificateurs, et les répétitions.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['control', 'keyboard', 'shortcut', 'keys', 'input'], + complexity: 'simple', + estimatedDuration: 500, + }, + }, + { + id: 'visual_condition', + name: 'Condition visuelle', + category: 'control' as VWBActionCategory, + description: 'Exécuter une branche conditionnelle selon la présence d\'un élément', + icon: '🔀', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Élément visuel à vérifier', + default: null, + }, + condition_type: { + type: 'string', + required: false, + description: 'Type de condition', + default: 'exists', + options: ['exists', 'not_exists', 'contains_text'], + }, + expected_text: { + type: 'string', + required: false, + description: 'Texte attendu (si condition = contains_text)', + default: '', + }, + confidence_threshold: { + type: 'number', + required: false, + description: 'Seuil de confiance minimum (0-100)', + default: 80, + min: 50, + max: 100, + }, + }, + examples: [ + { + name: 'Vérifier bouton actif', + description: 'Continuer si le bouton "Suivant" est visible', + parameters: { condition_type: 'exists', confidence_threshold: 80 }, + expectedResult: 'Branche "Si vrai" ou "Si faux" selon la présence', + }, + ], + documentation: 'Permet de créer des embranchements conditionnels basés sur la détection visuelle d\'éléments.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['control', 'condition', 'if-else', 'branch', 'vision'], + complexity: 'intermediate', + estimatedDuration: 2000, + }, + }, + { + id: 'loop_visual', + name: 'Boucle visuelle', + category: 'control' as VWBActionCategory, + description: 'Répéter des actions tant qu\'un élément est visible ou sur chaque occurrence', + icon: '🔁', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Élément visuel de référence pour la boucle', + default: null, + }, + loop_type: { + type: 'string', + required: false, + description: 'Type de boucle', + default: 'while_visible', + options: ['while_visible', 'while_not_visible', 'for_each_occurrence'], + }, + max_iterations: { + type: 'number', + required: false, + description: 'Nombre maximum d\'itérations', + default: 100, + min: 1, + max: 1000, + }, + }, + examples: [ + { + name: 'Traiter tous les éléments', + description: 'Boucler sur chaque ligne d\'une liste', + parameters: { loop_type: 'for_each_occurrence', max_iterations: 50 }, + expectedResult: 'Les actions internes sont répétées pour chaque occurrence', + }, + ], + documentation: 'Permet de répéter des actions basées sur la présence d\'éléments visuels.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['control', 'loop', 'iteration', 'repeat', 'vision'], + complexity: 'advanced', + estimatedDuration: 10000, + }, + }, + { + id: 'scroll_to_anchor', + name: 'Défiler vers élément', + category: 'control' as VWBActionCategory, + description: 'Défiler la page jusqu\'à ce qu\'un élément soit visible', + icon: '📜', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Élément vers lequel défiler', + default: null, + }, + scroll_behavior: { + type: 'string', + required: false, + description: 'Comportement du défilement', + default: 'smooth', + options: ['smooth', 'instant'], + }, + }, + examples: [], + documentation: 'Fait défiler la page pour rendre un élément visuel visible à l\'écran.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['control', 'scroll', 'navigation', 'vision'], + complexity: 'simple', + estimatedDuration: 2000, + }, + }, + + // ═══════════════════════════════════════════════════════════════════════════ + // CATÉGORIE: EXTRACTION DE DONNÉES (data) + // ═══════════════════════════════════════════════════════════════════════════ + { + id: 'extract_text', + name: 'Extraire texte', + category: 'data' as VWBActionCategory, + description: 'Extraire le texte d\'un élément visuel par OCR', + icon: '📤', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Zone contenant le texte à extraire', + default: null, + }, + ocr_language: { + type: 'string', + required: false, + description: 'Langue pour la reconnaissance de texte', + default: 'fra', + options: ['fra', 'eng', 'deu', 'spa', 'ita'], + }, + variable_name: { + type: 'string', + required: false, + description: 'Nom de la variable pour stocker le texte extrait', + default: 'extracted_text', + }, + }, + examples: [ + { + name: 'Extraire numéro de facture', + description: 'Lire un numéro de facture affiché à l\'écran', + parameters: { ocr_language: 'fra', variable_name: 'numero_facture' }, + expectedResult: 'Le texte est extrait et stocké dans la variable', + }, + ], + documentation: 'Utilise l\'OCR pour extraire du texte depuis une zone visuelle.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['data', 'text', 'extraction', 'ocr', 'vision'], + complexity: 'intermediate', + estimatedDuration: 3000, + }, + }, + { + id: 'extract_table', + name: 'Extraire tableau', + category: 'data' as VWBActionCategory, + description: 'Extraire les données d\'un tableau visuel', + icon: '📊', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Zone contenant le tableau', + default: null, + }, + has_headers: { + type: 'boolean', + required: false, + description: 'Le tableau a-t-il une ligne d\'en-tête', + default: true, + }, + variable_name: { + type: 'string', + required: false, + description: 'Nom de la variable pour stocker les données', + default: 'table_data', + }, + }, + examples: [ + { + name: 'Extraire liste de commandes', + description: 'Extraire un tableau de commandes avec colonnes', + parameters: { has_headers: true, variable_name: 'commandes' }, + expectedResult: 'Les données tabulaires sont extraites en JSON', + }, + ], + documentation: 'Extrait les données structurées d\'un tableau visuel.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['data', 'table', 'extraction', 'structured', 'vision'], + complexity: 'advanced', + estimatedDuration: 5000, + }, + }, + { + id: 'screenshot_evidence', + name: 'Capture d\'écran', + category: 'data' as VWBActionCategory, + description: 'Capturer une preuve visuelle de l\'état actuel', + icon: '📸', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: false, + description: 'Zone spécifique à capturer (optionnel, sinon écran entier)', + default: null, + }, + delay_before_seconds: { + type: 'number', + required: false, + description: 'Délai avant la capture (secondes)', + default: 0, + min: 0, + max: 10, + }, + description: { + type: 'string', + required: false, + description: 'Description de la capture', + default: '', + }, + save_path: { + type: 'string', + required: false, + description: 'Chemin de sauvegarde (optionnel)', + default: '', + }, + use_saved_capture: { + type: 'string', + required: false, + description: 'Utiliser une capture sauvegardée (nom de la capture)', + default: '', + }, + }, + examples: [ + { + name: 'Capture après délai', + description: 'Attendre 2 secondes avant de capturer (pour menu ouvert)', + parameters: { delay_before_seconds: 2, description: 'Menu contextuel ouvert' }, + expectedResult: 'La capture est prise après le délai', + }, + ], + documentation: 'Capture une preuve visuelle pour documentation ou audit. Peut attendre un délai avant la capture pour laisser le temps aux animations/menus de s\'afficher.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['data', 'screenshot', 'evidence', 'documentation'], + complexity: 'simple', + estimatedDuration: 2000, + }, + }, + { + id: 'download_to_folder', + name: 'Télécharger fichier', + category: 'data' as VWBActionCategory, + description: 'Télécharger un fichier vers un répertoire spécifié', + icon: '📥', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Bouton ou lien de téléchargement', + default: null, + }, + target_folder: { + type: 'string', + required: true, + description: 'Répertoire de destination', + default: '/home/user/Downloads', + }, + filename_pattern: { + type: 'string', + required: false, + description: 'Pattern pour renommer le fichier (optionnel)', + default: '', + }, + wait_for_completion: { + type: 'boolean', + required: false, + description: 'Attendre la fin du téléchargement', + default: true, + }, + timeout_ms: { + type: 'number', + required: false, + description: 'Timeout pour le téléchargement', + default: 60000, + min: 5000, + max: 600000, + }, + }, + examples: [ + { + name: 'Télécharger rapport PDF', + description: 'Télécharger un rapport dans le dossier Documents', + parameters: { target_folder: '/home/user/Documents/Rapports', wait_for_completion: true }, + expectedResult: 'Le fichier est téléchargé dans le dossier spécifié', + }, + ], + documentation: 'Déclenche un téléchargement et enregistre le fichier dans un répertoire choisi.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['data', 'download', 'file', 'save', 'vision'], + complexity: 'intermediate', + estimatedDuration: 30000, + }, + }, + + // ═══════════════════════════════════════════════════════════════════════════ + // CATÉGORIE: INTELLIGENCE ARTIFICIELLE (intelligence) + // ═══════════════════════════════════════════════════════════════════════════ + { + id: 'ai_analyze_text', + name: 'Analyser par IA', + category: 'intelligence' as VWBActionCategory, + description: 'Analyser du texte extrait par intelligence artificielle', + icon: '🤖', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Zone contenant le texte à analyser', + default: null, + }, + analysis_prompt: { + type: 'string', + required: true, + description: 'Instructions pour l\'analyse IA', + default: 'Résume ce texte en quelques points clés', + }, + output_format: { + type: 'string', + required: false, + description: 'Format de sortie souhaité', + default: 'text', + options: ['text', 'json', 'bullet_points', 'summary'], + }, + variable_name: { + type: 'string', + required: false, + description: 'Variable pour stocker le résultat', + default: 'ai_analysis_result', + }, + model: { + type: 'string', + required: false, + description: 'Modèle IA à utiliser', + default: 'claude-sonnet', + options: ['claude-sonnet', 'claude-haiku', 'claude-opus'], + }, + }, + examples: [ + { + name: 'Analyser compte rendu médical', + description: 'Extraire les informations clés d\'un compte rendu', + parameters: { + analysis_prompt: 'Extrait les diagnostics, traitements et recommandations de ce compte rendu médical. Retourne un JSON structuré.', + output_format: 'json', + variable_name: 'analyse_medical' + }, + expectedResult: 'Un objet JSON avec les informations structurées', + }, + { + name: 'Résumer un email', + description: 'Obtenir un résumé d\'un email long', + parameters: { + analysis_prompt: 'Résume cet email en 3 points maximum avec les actions requises', + output_format: 'bullet_points', + variable_name: 'resume_email' + }, + expectedResult: 'Liste à puces avec les points clés', + }, + ], + documentation: 'Utilise l\'IA pour analyser, résumer ou extraire des informations de texte capturé visuellement. Idéal pour traiter des comptes rendus, emails, documents.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['intelligence', 'ai', 'analysis', 'nlp', 'summary', 'vision'], + complexity: 'advanced', + estimatedDuration: 10000, + }, + }, + + // ═══════════════════════════════════════════════════════════════════════════ + // CATÉGORIE: BASE DE DONNÉES (database) + // ═══════════════════════════════════════════════════════════════════════════ + { + id: 'db_save_data', + name: 'Sauvegarder en BDD', + category: 'database' as VWBActionCategory, + description: 'Enregistrer des données extraites dans une base de données', + icon: '💾', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: false, + description: 'Zone source des données (optionnel si variable déjà définie)', + default: null, + }, + source_variable: { + type: 'string', + required: false, + description: 'Variable contenant les données à sauvegarder', + default: '', + }, + table_name: { + type: 'string', + required: true, + description: 'Nom de la table de destination', + default: '', + }, + field_mapping: { + type: 'string', + required: false, + description: 'Mapping des champs (JSON): {"champ_source": "colonne_bdd"}', + default: '{}', + }, + connection_name: { + type: 'string', + required: false, + description: 'Nom de la connexion BDD configurée', + default: 'default', + }, + mode: { + type: 'string', + required: false, + description: 'Mode d\'insertion', + default: 'insert', + options: ['insert', 'upsert', 'update'], + }, + }, + examples: [ + { + name: 'Enregistrer facture', + description: 'Sauvegarder les données d\'une facture extraite', + parameters: { + source_variable: 'facture_data', + table_name: 'factures', + field_mapping: '{"numero": "num_facture", "montant": "total_ttc", "date": "date_facture"}', + mode: 'insert' + }, + expectedResult: 'Une nouvelle ligne est créée dans la table factures', + }, + ], + documentation: 'Permet de persister des données extraites visuellement dans une base de données relationnelle.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['database', 'save', 'persist', 'insert', 'data'], + complexity: 'advanced', + estimatedDuration: 3000, + }, + }, + { + id: 'db_read_data', + name: 'Lire depuis BDD', + category: 'database' as VWBActionCategory, + description: 'Lire des données depuis une base de données', + icon: '📖', + parameters: { + table_name: { + type: 'string', + required: true, + description: 'Nom de la table à interroger', + default: '', + }, + query_filter: { + type: 'string', + required: false, + description: 'Filtre SQL WHERE (sans le mot-clé WHERE)', + default: '', + }, + columns: { + type: 'string', + required: false, + description: 'Colonnes à récupérer (séparées par virgule, ou * pour tout)', + default: '*', + }, + limit: { + type: 'number', + required: false, + description: 'Nombre maximum de lignes', + default: 100, + min: 1, + max: 10000, + }, + variable_name: { + type: 'string', + required: true, + description: 'Variable pour stocker les résultats', + default: 'db_results', + }, + connection_name: { + type: 'string', + required: false, + description: 'Nom de la connexion BDD configurée', + default: 'default', + }, + }, + examples: [ + { + name: 'Récupérer client par email', + description: 'Chercher un client dans la base par son email', + parameters: { + table_name: 'clients', + query_filter: "email = '${email_extrait}'", + columns: 'id, nom, prenom, telephone', + variable_name: 'client_info' + }, + expectedResult: 'Les données du client sont stockées dans la variable', + }, + ], + documentation: 'Permet de récupérer des données depuis la base pour les utiliser dans le workflow.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['database', 'read', 'query', 'select', 'data'], + complexity: 'intermediate', + estimatedDuration: 2000, + }, + }, + + // ═══════════════════════════════════════════════════════════════════════════ + // CATÉGORIE: VALIDATION (validation) + // ═══════════════════════════════════════════════════════════════════════════ + { + id: 'verify_element_exists', + name: 'Vérifier existence', + category: 'validation' as VWBActionCategory, + description: 'Vérifier qu\'un élément visuel existe ou non', + icon: '✅', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Élément à vérifier', + default: null, + }, + should_exist: { + type: 'boolean', + required: false, + description: 'L\'élément doit-il exister', + default: true, + }, + fail_on_mismatch: { + type: 'boolean', + required: false, + description: 'Échouer si la vérification ne correspond pas', + default: true, + }, + }, + examples: [], + documentation: 'Vérifie la présence ou l\'absence d\'un élément visuel.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['validation', 'verification', 'exists', 'vision'], + complexity: 'simple', + estimatedDuration: 2000, + }, + }, + { + id: 'verify_text_content', + name: 'Vérifier texte', + category: 'validation' as VWBActionCategory, + description: 'Vérifier le contenu textuel d\'un élément', + icon: '📝', + parameters: { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Élément contenant le texte', + default: null, + }, + expected_text: { + type: 'string', + required: true, + description: 'Texte attendu', + default: '', + }, + match_mode: { + type: 'string', + required: false, + description: 'Type de correspondance', + default: 'contains', + options: ['exact', 'contains', 'starts_with', 'ends_with', 'regex'], + }, + case_sensitive: { + type: 'boolean', + required: false, + description: 'Sensible à la casse (majuscules ≠ minuscules)', + default: false, + }, + ocr_mode: { + type: 'string', + required: false, + description: 'Mode OCR: ollama (GPU, haute qualité) ou easyocr (rapide)', + default: 'ollama', + options: ['ollama', 'easyocr'], + }, + ollama_model: { + type: 'string', + required: false, + description: 'Modèle Ollama pour l\'OCR', + default: 'qwen2.5-vl:7b', + }, + }, + examples: [], + documentation: 'Vérifie que le texte d\'un élément correspond aux attentes. Utilise l\'OCR via Ollama (GPU) ou EasyOCR.', + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro', + createdAt: '2026-01-13', + updatedAt: '2026-01-13', + tags: ['validation', 'text', 'content', 'vision'], + complexity: 'intermediate', + estimatedDuration: 3000, + }, + }, +]; + +/** + * Catégories du catalogue 100% visuel + */ +export const STATIC_CATALOG_CATEGORIES = [ + { + id: 'vision_ui' as VWBActionCategory, + name: 'Interactions Visuelles', + description: 'Cliquer, saisir, glisser-déposer sur des éléments visuels', + icon: '🖱️', + actionCount: 8, + }, + { + id: 'control' as VWBActionCategory, + name: 'Contrôle de Flux', + description: 'Conditions, boucles, raccourcis clavier et synchronisation', + icon: '🔀', + actionCount: 5, + }, + { + id: 'data' as VWBActionCategory, + name: 'Extraction de Données', + description: 'Extraire texte, tableaux, télécharger des fichiers', + icon: '📊', + actionCount: 4, + }, + { + id: 'intelligence' as VWBActionCategory, + name: 'Intelligence IA', + description: 'Analyse et traitement intelligent par IA', + icon: '🤖', + actionCount: 1, + }, + { + id: 'database' as VWBActionCategory, + name: 'Base de Données', + description: 'Lire et enregistrer des données en base', + icon: '💾', + actionCount: 2, + }, + { + id: 'validation' as VWBActionCategory, + name: 'Validation', + description: 'Vérifier la présence et le contenu des éléments', + icon: '✅', + actionCount: 2, + }, +]; + +// Fonctions utilitaires +export function getStaticCatalogActions(): StaticCatalogAction[] { + return [...STATIC_CATALOG_ACTIONS]; +} + +export function getStaticActionsByCategory(category: VWBActionCategory): StaticCatalogAction[] { + return STATIC_CATALOG_ACTIONS.filter(action => action.category === category); +} + +export function getStaticActionById(actionId: string): StaticCatalogAction | null { + return STATIC_CATALOG_ACTIONS.find(action => action.id === actionId) || null; +} + +/** + * Recherche d'actions avec fallback intelligent + */ +export function findActionWithFallback(actionId: string): StaticCatalogAction | null { + let action = getStaticActionById(actionId); + if (action) { + return action; + } + + const normalizedId = actionId.toLowerCase().replace(/[_-]/g, ''); + action = STATIC_CATALOG_ACTIONS.find(a => + a.id.toLowerCase().replace(/[_-]/g, '') === normalizedId || + a.name.toLowerCase().replace(/[_\s-]/g, '') === normalizedId + ) || null; + + if (action) { + return { + ...action, + fallbackMetadata: { + isFallback: true, + fallbackReason: 'Correspondance par similarité de nom', + originalActionId: actionId, + createdAt: Date.now(), + confidence: 0.8 + } + }; + } + + const searchTerms = actionId.toLowerCase().split(/[_-]/); + action = STATIC_CATALOG_ACTIONS.find(a => { + const tags = a.metadata?.tags || []; + return searchTerms.some(term => + tags.some(tag => tag.toLowerCase().includes(term)) || + a.description.toLowerCase().includes(term) + ); + }) || null; + + if (action) { + return { + ...action, + fallbackMetadata: { + isFallback: true, + fallbackReason: 'Correspondance par mots-clés', + originalActionId: actionId, + createdAt: Date.now(), + confidence: 0.6 + } + }; + } + + return null; +} + +/** + * Crée une action de fallback générique + */ +export function createFallbackAction(actionId: string): StaticCatalogAction { + const id = actionId.toLowerCase(); + let category: VWBActionCategory = 'vision_ui'; + let icon = '🔧'; + let description = `Action VWB: ${actionId}`; + + if (id.includes('click')) { + category = 'vision_ui'; + icon = '🖱️'; + description = 'Action de clic sur élément visuel'; + } else if (id.includes('type') || id.includes('text')) { + category = 'vision_ui'; + icon = '⌨️'; + description = 'Action de saisie de texte'; + } else if (id.includes('wait') || id.includes('delay')) { + category = 'control'; + icon = '⏳'; + description = 'Action d\'attente ou de délai'; + } else if (id.includes('extract') || id.includes('get')) { + category = 'data'; + icon = '📤'; + description = 'Action d\'extraction de données'; + } else if (id.includes('verify') || id.includes('check')) { + category = 'validation'; + icon = '✅'; + description = 'Action de validation'; + } else if (id.includes('ai') || id.includes('analyz')) { + category = 'intelligence'; + icon = '🤖'; + description = 'Action d\'analyse IA'; + } else if (id.includes('db') || id.includes('database') || id.includes('save')) { + category = 'database'; + icon = '💾'; + description = 'Action base de données'; + } + + return { + id: actionId, + name: actionId.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), + category, + description, + icon, + parameters: { + ...(category === 'vision_ui' && { + visual_anchor: { + type: 'VWBVisualAnchor', + required: true, + description: 'Élément visuel cible', + default: null, + } + }), + }, + examples: [], + documentation: `Action générée automatiquement pour ${actionId}`, + metadata: { + version: '3.0.0', + author: 'Dom, Alice, Kiro - Fallback Generator', + createdAt: new Date().toISOString().split('T')[0], + updatedAt: new Date().toISOString().split('T')[0], + tags: ['fallback', 'generated', category], + complexity: 'simple', + estimatedDuration: 2000, + }, + fallbackMetadata: { + isFallback: true, + fallbackReason: 'Action générée automatiquement', + originalActionId: actionId, + createdAt: Date.now(), + confidence: 0.3 + } + }; +} + +export function validateStaticAction(action: StaticCatalogAction): { + isValid: boolean; + errors: string[]; + warnings: string[]; +} { + const errors: string[] = []; + const warnings: string[] = []; + + if (!action.id) errors.push('ID manquant'); + if (!action.name) errors.push('Nom manquant'); + if (!action.category) errors.push('Catégorie manquante'); + if (!action.description) errors.push('Description manquante'); + + if (action.parameters) { + Object.entries(action.parameters).forEach(([paramName, paramConfig]) => { + if (!paramConfig.type) { + errors.push(`Type manquant pour le paramètre ${paramName}`); + } + }); + } + + if (action.fallbackMetadata?.isFallback) { + warnings.push(`Action de fallback (confiance: ${action.fallbackMetadata.confidence})`); + } + + return { + isValid: errors.length === 0, + errors, + warnings + }; +} + +export function searchStaticActions(searchTerm: string): StaticCatalogAction[] { + if (!searchTerm || searchTerm.trim().length === 0) { + return []; + } + + const term = searchTerm.toLowerCase().trim(); + + return STATIC_CATALOG_ACTIONS.filter(action => + action.name.toLowerCase().includes(term) || + action.description.toLowerCase().includes(term) || + (action.metadata?.tags || []).some((tag: string) => tag.toLowerCase().includes(term)) + ); +} + +export function getStaticCatalogCategories() { + return [...STATIC_CATALOG_CATEGORIES]; +} + +export function getStaticCatalogStats() { + return { + totalActions: STATIC_CATALOG_ACTIONS.length, + categories: STATIC_CATALOG_CATEGORIES.length, + actionsByCategory: STATIC_CATALOG_CATEGORIES.reduce((acc, cat) => { + acc[cat.id] = cat.actionCount; + return acc; + }, {} as Record), + version: '3.0.0', + mode: 'static', + lastUpdated: '2026-01-13', + features: ['100% visual', 'AI analysis', 'database integration', 'file download'], + }; +} + +export const staticCatalog = { + actions: STATIC_CATALOG_ACTIONS, + categories: STATIC_CATALOG_CATEGORIES, + getActions: getStaticCatalogActions, + getActionsByCategory: getStaticActionsByCategory, + getActionById: getStaticActionById, + findActionWithFallback, + createFallbackAction, + validateStaticAction, + searchActions: searchStaticActions, + getCategories: getStaticCatalogCategories, + getStats: getStaticCatalogStats, +}; + +export default staticCatalog;