feat: VWB — aide outil (?), croix suppression, plein écran, zones détection

- Bouton ? sur chaque nœud : tooltip avec description + paramètres typés
- Croix rouge visible (fix overflow React Flow)
- Sélection plein écran avec détection auto des éléments UI
- Zones détectées affichées sur l'aperçu de capture
- 32 actions documentées en français avec paramètres typés
- Pruning candidats VLM : max 80 avant classification (3x plus rapide)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-03-17 08:13:46 +01:00
parent fb648e730f
commit 1e18194e31
5 changed files with 466 additions and 67 deletions

View File

@@ -39,6 +39,11 @@ export default function CapturePanel({
const [currentCapture, setCurrentCapture] = useState<Capture | null>(null);
const [timerSeconds, setTimerSeconds] = useState(0);
const [countdown, setCountdown] = useState<number | null>(null);
// Éléments détectés sur l'aperçu miniature
const [previewElements, setPreviewElements] = useState<UIElement[]>([]);
const [isDetectingPreview, setIsDetectingPreview] = useState(false);
const previewImgRef = useRef<HTMLImageElement>(null);
const [previewScale, setPreviewScale] = useState({ x: 1, y: 1 });
const isDebugMode = executionMode === 'debug';
@@ -68,6 +73,43 @@ export default function CapturePanel({
}
}, [capture]);
// Détecter les éléments UI quand une capture arrive
useEffect(() => {
if (!currentCapture) {
setPreviewElements([]);
return;
}
const runDetection = async () => {
setIsDetectingPreview(true);
try {
const { detectUIElements } = await import('../services/uiDetection');
const result = await detectUIElements(
`data:image/png;base64,${currentCapture.screenshot_base64}`,
{ threshold: 0.35 }
);
setPreviewElements(result.elements);
} catch (err) {
console.error('Détection aperçu échouée:', err);
setPreviewElements([]);
} finally {
setIsDetectingPreview(false);
}
};
runDetection();
}, [currentCapture]);
// Calculer le scale de l'aperçu quand l'image est chargée
const handlePreviewImageLoad = () => {
if (previewImgRef.current) {
setPreviewScale({
x: previewImgRef.current.width / previewImgRef.current.naturalWidth,
y: previewImgRef.current.height / previewImgRef.current.naturalHeight
});
}
};
const handleTimerCapture = () => {
if (timerSeconds === 0) {
onCapture();
@@ -117,16 +159,43 @@ export default function CapturePanel({
</button>
</div>
{/* Aperçu de la capture */}
{/* Aperçu de la capture avec détection */}
{currentCapture && (
<div className="capture-preview">
<img
src={`data:image/png;base64,${currentCapture.screenshot_base64}`}
alt="Capture"
onClick={() => setIsFullscreen(true)}
/>
<div className="capture-preview-container">
<img
ref={previewImgRef}
src={`data:image/png;base64,${currentCapture.screenshot_base64}`}
alt="Capture"
onClick={() => setIsFullscreen(true)}
onLoad={handlePreviewImageLoad}
/>
{/* Bounding boxes des éléments détectés sur l'aperçu miniature */}
{previewElements.map((elem) => (
<div
key={elem.id}
className="capture-preview-bbox"
style={{
position: 'absolute',
left: elem.bbox.x1 * previewScale.x,
top: elem.bbox.y1 * previewScale.y,
width: (elem.bbox.x2 - elem.bbox.x1) * previewScale.x,
height: (elem.bbox.y2 - elem.bbox.y1) * previewScale.y,
}}
title={`#${elem.id} (${(elem.confidence * 100).toFixed(0)}%)`}
/>
))}
{isDetectingPreview && (
<div className="capture-preview-detecting">Détection...</div>
)}
</div>
<p className="capture-info">
{currentCapture.width}x{currentCapture.height}
{previewElements.length > 0 && (
<span className="capture-elements-count">
{previewElements.length} éléments
</span>
)}
<button onClick={() => setIsFullscreen(true)}>Plein écran</button>
</p>
</div>
@@ -139,13 +208,13 @@ export default function CapturePanel({
{/* Zone de détection */}
{detectionZone && (
<div className="detection-zone-info">
<span>📍 Zone de détection: {detectionZone.width}x{detectionZone.height}</span>
<button onClick={() => onSetDetectionZone?.(null)}></button>
<span>Zone de détection: {detectionZone.width}x{detectionZone.height}</span>
<button onClick={() => onSetDetectionZone?.(null)}>Effacer</button>
</div>
)}
{!detectionZone && currentCapture && (executionMode === 'intelligent' || executionMode === 'debug') && (
<p className="capture-hint zone-hint">
💡 Cliquez sur "Plein écran" puis "Zone de détection" pour cibler une zone
Cliquez sur "Plein écran" puis "Zone de détection" pour cibler une zone
</p>
)}
@@ -217,11 +286,10 @@ function FullscreenSelector({
const [isDetecting, setIsDetecting] = useState(false);
const [imageScale, setImageScale] = useState({ x: 1, y: 1 });
const [selectionMode, setSelectionMode] = useState<'anchor' | 'zone'>('anchor');
const [showDetection, setShowDetection] = useState(true);
// Lancer la détection en mode Debug
// Lancer la détection automatiquement (tous modes)
useEffect(() => {
if (!debugMode) return;
const runDetection = async () => {
setIsDetecting(true);
try {
@@ -239,7 +307,7 @@ function FullscreenSelector({
};
runDetection();
}, [debugMode, capture.screenshot_base64]);
}, [capture.screenshot_base64]);
// Calculer le scale quand l'image est chargée
const handleImageLoad = () => {
@@ -339,28 +407,36 @@ function FullscreenSelector({
<div className="fullscreen-header">
<div className="header-left">
<span>
{debugMode && isDetecting && '🔍 Détection en cours... '}
{debugMode && !isDetecting && `🎯 ${detectedElements.length} éléments - `}
{isDetecting && 'Détection en cours... '}
{!isDetecting && detectedElements.length > 0 && `${detectedElements.length} éléments - `}
{selectionMode === 'zone'
? '✂️ Dessinez la zone de détection'
: (enabled ? 'Dessinez un rectangle pour l\'ancre' : 'Sélectionnez d\'abord une étape')}
? 'Dessinez la zone de détection'
: (enabled ? 'Dessinez un rectangle pour l\'ancre ou cliquez sur un élément détecté' : 'Sélectionnez d\'abord une étape')}
</span>
</div>
<div className="header-center">
{/* Bouton afficher/masquer la détection */}
<button
className={`zone-select-btn ${showDetection ? 'active' : ''}`}
onClick={() => setShowDetection(!showDetection)}
style={showDetection ? { background: 'var(--secondary)', borderColor: 'var(--secondary)', color: 'white' } : {}}
>
{showDetection ? 'Masquer détection' : 'Afficher détection'}
</button>
{onSetDetectionZone && (
<>
<button
className={`zone-select-btn ${selectionMode === 'zone' ? 'active' : ''}`}
onClick={() => setSelectionMode(selectionMode === 'zone' ? 'anchor' : 'zone')}
>
{selectionMode === 'zone' ? 'Annuler' : '✂️ Zone de détection'}
{selectionMode === 'zone' ? 'Annuler' : 'Zone de détection'}
</button>
{detectionZone && (
<button
className="zone-clear-btn"
onClick={() => onSetDetectionZone(null)}
>
Effacer zone
Effacer zone
</button>
)}
</>
@@ -385,8 +461,8 @@ function FullscreenSelector({
style={{ display: 'block' }}
/>
{/* Overlay des éléments détectés en mode Debug */}
{debugMode && detectedElements.map((elem) => (
{/* Overlay des éléments détectés — visible dans tous les modes */}
{showDetection && detectedElements.map((elem) => (
<div
key={elem.id}
className="fullscreen-detection-bbox"
@@ -405,7 +481,7 @@ function FullscreenSelector({
e.stopPropagation();
handleElementClick(elem);
}}
title={`ID: ${elem.id} | Confiance: ${(elem.confidence * 100).toFixed(0)}%`}
title={`#${elem.id} | Confiance: ${(elem.confidence * 100).toFixed(0)}%`}
>
<span style={{
position: 'absolute',

View File

@@ -1,4 +1,4 @@
import { memo } from 'react';
import { memo, useState, useEffect, useRef } from 'react';
import { Handle, Position } from '@xyflow/react';
import type { Step } from '../types';
import { ACTIONS } from '../types';
@@ -16,8 +16,64 @@ function StepNode({ data, selected }: StepNodeProps) {
const isDataLoop = step.action_type === 'db_foreach';
const isImport = step.action_type === 'import_excel';
// État du tooltip d'aide
const [showHelp, setShowHelp] = useState(false);
const helpRef = useRef<HTMLDivElement>(null);
// Fermer le tooltip en cliquant à l'extérieur
useEffect(() => {
if (!showHelp) return;
const handleClickOutside = (e: MouseEvent) => {
if (helpRef.current && !helpRef.current.contains(e.target as Node)) {
setShowHelp(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [showHelp]);
return (
<div className={`step-node ${selected ? 'selected' : ''} ${isConditional ? 'conditional' : ''} ${isDataLoop ? 'data-loop' : ''} ${isImport ? 'data-import' : ''}`}>
{/* Bouton aide (?) — toujours visible quand sélectionné */}
{selected && action && (
<button
className="step-node-help"
title="Documentation de l'outil"
onClick={(e) => {
e.stopPropagation();
setShowHelp(!showHelp);
}}
>
?
</button>
)}
{/* Tooltip d'aide */}
{showHelp && action && (
<div className="step-node-tooltip" ref={helpRef} onClick={(e) => e.stopPropagation()}>
<div className="tooltip-header">
<span className="tooltip-icon">{action.icon}</span>
<span className="tooltip-title">{action.label}</span>
</div>
<p className="tooltip-description">{action.description}</p>
{action.needsAnchor && (
<div className="tooltip-anchor-badge">Ancre visuelle requise</div>
)}
{action.params.length > 0 && (
<div className="tooltip-params">
<div className="tooltip-params-title">Paramètres :</div>
{action.params.map((p) => (
<div key={p.name} className="tooltip-param">
<span className="tooltip-param-name">{p.name}</span>
<span className="tooltip-param-type">{p.type}</span>
<span className="tooltip-param-desc">{p.description}</span>
</div>
))}
</div>
)}
</div>
)}
{/* Bouton supprimer */}
{selected && (
<button

View File

@@ -416,14 +416,20 @@ body {
font-size: 12px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: relative;
overflow: visible;
}
/* Forcer overflow visible sur le wrapper React Flow */
.react-flow__node {
overflow: visible !important;
}
.step-node-delete {
position: absolute;
top: -8px;
right: -8px;
width: 20px;
height: 20px;
top: -10px;
right: -10px;
width: 22px;
height: 22px;
border-radius: 50%;
background: #e53935;
color: white;
@@ -434,13 +440,141 @@ body {
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
z-index: 50;
box-shadow: 0 2px 6px rgba(0,0,0,0.35);
padding: 0;
}
.step-node-delete:hover {
background: #c62828;
transform: scale(1.1);
transform: scale(1.2);
}
/* Bouton d'aide (?) */
.step-node-help {
position: absolute;
top: -10px;
left: -10px;
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--primary);
color: white;
border: 2px solid white;
font-size: 12px;
font-weight: 700;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
box-shadow: 0 2px 6px rgba(0,0,0,0.35);
padding: 0;
}
.step-node-help:hover {
background: var(--primary-dark);
transform: scale(1.2);
}
/* Tooltip documentation outil */
.step-node-tooltip {
position: absolute;
top: -8px;
left: calc(100% + 12px);
width: 250px;
background: var(--bg-paper);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.2);
padding: 0.75rem;
z-index: 100;
font-size: 0.8rem;
color: var(--text-primary);
pointer-events: auto;
}
.step-node-tooltip .tooltip-header {
display: flex;
align-items: center;
gap: 0.4rem;
margin-bottom: 0.5rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--border);
}
.step-node-tooltip .tooltip-icon {
font-size: 1.2rem;
}
.step-node-tooltip .tooltip-title {
font-weight: 700;
font-size: 0.9rem;
}
.step-node-tooltip .tooltip-description {
color: var(--text-secondary);
font-size: 0.78rem;
line-height: 1.4;
margin-bottom: 0.5rem;
}
.step-node-tooltip .tooltip-anchor-badge {
display: inline-block;
background: rgba(244, 67, 54, 0.1);
color: var(--error);
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.step-node-tooltip .tooltip-params {
margin-top: 0.25rem;
}
.step-node-tooltip .tooltip-params-title {
font-weight: 600;
font-size: 0.75rem;
color: var(--text-primary);
margin-bottom: 0.3rem;
}
.step-node-tooltip .tooltip-param {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
align-items: baseline;
padding: 0.2rem 0;
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.step-node-tooltip .tooltip-param:last-child {
border-bottom: none;
}
.step-node-tooltip .tooltip-param-name {
font-weight: 600;
font-size: 0.75rem;
color: var(--primary);
font-family: monospace;
}
.step-node-tooltip .tooltip-param-type {
font-size: 0.65rem;
background: var(--bg-sidebar);
padding: 0.1rem 0.3rem;
border-radius: 3px;
color: var(--text-secondary);
}
.step-node-tooltip .tooltip-param-desc {
width: 100%;
font-size: 0.7rem;
color: var(--text-secondary);
line-height: 1.3;
}
.step-node.selected {
@@ -756,11 +890,45 @@ body {
margin-bottom: 1rem;
}
/* Conteneur relatif pour les bboxes de détection sur l'aperçu */
.capture-preview-container {
position: relative;
overflow: hidden;
border-radius: 4px;
border: 1px solid var(--border);
}
.capture-preview img {
width: 100%;
border-radius: 4px;
display: block;
cursor: pointer;
border: 1px solid var(--border);
}
/* Bounding boxes sur l'aperçu miniature */
.capture-preview-bbox {
border: 1px solid rgba(233, 69, 96, 0.6);
background: rgba(233, 69, 96, 0.08);
pointer-events: none;
}
/* Indicateur de détection en cours */
.capture-preview-detecting {
position: absolute;
top: 4px;
right: 4px;
background: var(--warning);
color: white;
font-size: 0.65rem;
padding: 0.15rem 0.4rem;
border-radius: 3px;
font-weight: 600;
}
/* Compteur d'éléments détectés */
.capture-elements-count {
color: var(--secondary);
font-weight: 500;
margin-left: 0.25rem;
}
.capture-info {

View File

@@ -61,64 +61,152 @@ export interface ActionDefinition {
type: ActionType;
label: string;
icon: string;
description: string;
category: 'mouse' | 'keyboard' | 'wait' | 'data' | 'logic' | 'ai' | 'llm' | 'validation';
needsAnchor: boolean;
params: string[];
params: { name: string; type: string; description: string }[];
}
export const ACTIONS: ActionDefinition[] = [
// === SOURIS ===
{ type: 'click_anchor', label: 'Clic', icon: '🖱️', category: 'mouse', needsAnchor: true, params: [] },
{ type: 'double_click_anchor', label: 'Double-clic', icon: '🖱️', category: 'mouse', needsAnchor: true, params: [] },
{ type: 'right_click_anchor', label: 'Clic droit', icon: '🖱️', category: 'mouse', needsAnchor: true, params: [] },
{ type: 'hover_anchor', label: 'Survol', icon: '👆', category: 'mouse', needsAnchor: true, params: [] },
{ type: 'drag_drop_anchor', label: 'Glisser-déposer', icon: '↔️', category: 'mouse', needsAnchor: true, params: ['target_anchor'] },
{ type: 'scroll_to_anchor', label: 'Défiler vers', icon: '📜', category: 'mouse', needsAnchor: true, params: [] },
{ type: 'focus_anchor', label: 'Focus', icon: '🎯', category: 'mouse', needsAnchor: true, params: [] },
{ type: 'click_anchor', label: 'Clic', icon: '🖱️', description: 'Clic gauche sur un élément visuel identifié par son ancre.', category: 'mouse', needsAnchor: true, params: [] },
{ type: 'double_click_anchor', label: 'Double-clic', icon: '🖱️', description: 'Double-clic sur un élément visuel.', category: 'mouse', needsAnchor: true, params: [] },
{ type: 'right_click_anchor', label: 'Clic droit', icon: '🖱️', description: 'Clic droit pour ouvrir le menu contextuel.', category: 'mouse', needsAnchor: true, params: [] },
{ type: 'hover_anchor', label: 'Survol', icon: '👆', description: 'Déplace la souris sur l\'élément sans cliquer (utile pour les tooltips).', category: 'mouse', needsAnchor: true, params: [] },
{ type: 'drag_drop_anchor', label: 'Glisser-déposer', icon: '↔️', description: 'Glisse un élément vers une cible (drag & drop).', category: 'mouse', needsAnchor: true, params: [
{ name: 'target_anchor', type: 'string', description: 'ID de l\'ancre cible du drop' }
] },
{ type: 'scroll_to_anchor', label: 'Défiler vers', icon: '📜', description: 'Fait défiler la page jusqu\'à ce que l\'élément soit visible.', category: 'mouse', needsAnchor: true, params: [] },
{ type: 'focus_anchor', label: 'Focus', icon: '🎯', description: 'Donne le focus clavier à l\'élément (champ de saisie, bouton).', category: 'mouse', needsAnchor: true, params: [] },
// === CLAVIER ===
{ type: 'type_text', label: 'Saisir texte', icon: '⌨️', category: 'keyboard', needsAnchor: false, params: ['text'] },
{ type: 'type_secret', label: 'Saisir secret', icon: '🔐', category: 'keyboard', needsAnchor: false, params: ['secret_key'] },
{ type: 'keyboard_shortcut', label: 'Raccourci clavier', icon: '⌘', category: 'keyboard', needsAnchor: false, params: ['keys'] },
{ type: 'type_text', label: 'Saisir texte', icon: '⌨️', description: 'Tape du texte au clavier. Supporte les variables {{nom}}.', category: 'keyboard', needsAnchor: false, params: [
{ name: 'text', type: 'string', description: 'Texte à saisir (supporte les variables)' }
] },
{ type: 'type_secret', label: 'Saisir secret', icon: '🔐', description: 'Tape un secret (mot de passe) sans l\'afficher dans les logs.', category: 'keyboard', needsAnchor: false, params: [
{ name: 'secret_key', type: 'string', description: 'Clé du secret dans le coffre-fort' }
] },
{ type: 'keyboard_shortcut', label: 'Raccourci clavier', icon: '⌘', description: 'Exécute une combinaison de touches (ex: Ctrl+C, Alt+F4).', category: 'keyboard', needsAnchor: false, params: [
{ name: 'keys', type: 'string', description: 'Combinaison de touches (ex: ctrl+c, alt+tab)' }
] },
// === ATTENTE ===
{ type: 'wait_for_anchor', label: 'Attendre élément', icon: '⏳', category: 'wait', needsAnchor: true, params: ['timeout_ms'] },
{ type: 'wait_for_anchor', label: 'Attendre élément', icon: '⏳', description: 'Attend qu\'un élément visuel apparaisse à l\'écran.', category: 'wait', needsAnchor: true, params: [
{ name: 'timeout_ms', type: 'number', description: 'Délai max d\'attente en millisecondes' }
] },
// === EXTRACTION DE DONNÉES ===
{ type: 'extract_text', label: 'Extraire texte', icon: '📋', category: 'data', needsAnchor: true, params: ['variable_name'] },
{ type: 'extract_table', label: 'Extraire tableau', icon: '📊', category: 'data', needsAnchor: true, params: ['variable_name'] },
{ type: 'screenshot_evidence', label: 'Capture preuve', icon: '📸', category: 'data', needsAnchor: false, params: ['filename'] },
{ type: 'download_to_folder', label: 'Télécharger', icon: '💾', category: 'data', needsAnchor: false, params: ['folder_path'] },
{ type: 'extract_text', label: 'Extraire texte', icon: '📋', description: 'Extrait le texte visible dans la zone de l\'ancre via OCR.', category: 'data', needsAnchor: true, params: [
{ name: 'variable_name', type: 'string', description: 'Nom de la variable pour stocker le résultat' }
] },
{ type: 'extract_table', label: 'Extraire tableau', icon: '📊', description: 'Extrait un tableau structuré depuis la zone de l\'ancre.', category: 'data', needsAnchor: true, params: [
{ name: 'variable_name', type: 'string', description: 'Nom de la variable pour stocker le tableau' }
] },
{ type: 'screenshot_evidence', label: 'Capture preuve', icon: '📸', description: 'Prend une capture d\'écran comme preuve d\'exécution.', category: 'data', needsAnchor: false, params: [
{ name: 'filename', type: 'string', description: 'Nom du fichier image de sortie' }
] },
{ type: 'download_to_folder', label: 'Télécharger', icon: '💾', description: 'Télécharge un fichier dans un dossier spécifié.', category: 'data', needsAnchor: false, params: [
{ name: 'folder_path', type: 'string', description: 'Chemin du dossier de destination' }
] },
// === LOGIQUE ===
{ type: 'visual_condition', label: 'Condition visuelle', icon: '🔀', category: 'logic', needsAnchor: true, params: ['on_found', 'on_not_found'] },
{ type: 'loop_visual', label: 'Boucle visuelle', icon: '🔁', category: 'logic', needsAnchor: true, params: ['max_iterations'] },
{ type: 'visual_condition', label: 'Condition visuelle', icon: '🔀', description: 'Branchement conditionnel : si l\'ancre est trouvée, suit la sortie bas ; sinon, la sortie droite.', category: 'logic', needsAnchor: true, params: [
{ name: 'on_found', type: 'string', description: 'ID de l\'étape si l\'élément est trouvé' },
{ name: 'on_not_found', type: 'string', description: 'ID de l\'étape si l\'élément n\'est pas trouvé' }
] },
{ type: 'loop_visual', label: 'Boucle visuelle', icon: '🔁', description: 'Répète les étapes connectées tant que l\'ancre est visible.', category: 'logic', needsAnchor: true, params: [
{ name: 'max_iterations', type: 'number', description: 'Nombre maximum d\'itérations' }
] },
// === INTELLIGENCE ARTIFICIELLE ===
{ type: 'ai_ocr', label: 'OCR Intelligent', icon: '📝', category: 'ai', needsAnchor: true, params: ['variable_name', 'model', 'language'] },
{ type: 'ai_summarize', label: 'Résumé IA', icon: '📋', category: 'ai', needsAnchor: false, params: ['input_text', 'variable_name', 'model', 'max_length'] },
{ type: 'ai_extract', label: 'Extraction IA', icon: '🔍', category: 'ai', needsAnchor: true, params: ['prompt', 'variable_name', 'model', 'output_format'] },
{ type: 'ai_classify', label: 'Classification IA', icon: '🏷️', category: 'ai', needsAnchor: false, params: ['input_text', 'categories', 'variable_name', 'model'] },
{ type: 'ai_analyze_text', label: 'Analyse complète', icon: '🧠', category: 'ai', needsAnchor: false, params: ['prompt', 'variable_name', 'model'] },
{ type: 'ai_custom', label: 'IA Personnalisée', icon: '⚙️', category: 'ai', needsAnchor: false, params: ['prompt', 'input_text', 'variable_name', 'model', 'system_prompt'] },
{ type: 'ai_ocr', label: 'OCR Intelligent', icon: '📝', description: 'Reconnaissance de texte par IA sur la zone de l\'ancre.', category: 'ai', needsAnchor: true, params: [
{ name: 'variable_name', type: 'string', description: 'Variable pour le texte reconnu' },
{ name: 'model', type: 'string', description: 'Modèle Ollama à utiliser' },
{ name: 'language', type: 'string', description: 'Langue du texte (fr, en, etc.)' }
] },
{ type: 'ai_summarize', label: 'Résumé IA', icon: '📋', description: 'Génère un résumé du texte extrait via un modèle LLM.', category: 'ai', needsAnchor: false, params: [
{ name: 'input_text', type: 'string', description: 'Texte ou variable à résumer' },
{ name: 'variable_name', type: 'string', description: 'Variable pour le résumé' },
{ name: 'model', type: 'string', description: 'Modèle Ollama' },
{ name: 'max_length', type: 'number', description: 'Longueur max du résumé' }
] },
{ type: 'ai_extract', label: 'Extraction IA', icon: '🔍', description: 'Extrait des informations structurées via un prompt IA.', category: 'ai', needsAnchor: true, params: [
{ name: 'prompt', type: 'string', description: 'Instruction d\'extraction' },
{ name: 'variable_name', type: 'string', description: 'Variable de sortie' },
{ name: 'model', type: 'string', description: 'Modèle Ollama' },
{ name: 'output_format', type: 'string', description: 'Format de sortie (json, text, list)' }
] },
{ type: 'ai_classify', label: 'Classification IA', icon: '🏷️', description: 'Classe un texte dans des catégories prédéfinies.', category: 'ai', needsAnchor: false, params: [
{ name: 'input_text', type: 'string', description: 'Texte à classifier' },
{ name: 'categories', type: 'string', description: 'Catégories (séparées par des virgules)' },
{ name: 'variable_name', type: 'string', description: 'Variable de sortie' },
{ name: 'model', type: 'string', description: 'Modèle Ollama' }
] },
{ type: 'ai_analyze_text', label: 'Analyse complète', icon: '🧠', description: 'Analyse approfondie d\'un texte avec un prompt personnalisé.', category: 'ai', needsAnchor: false, params: [
{ name: 'prompt', type: 'string', description: 'Prompt d\'analyse' },
{ name: 'variable_name', type: 'string', description: 'Variable de sortie' },
{ name: 'model', type: 'string', description: 'Modèle Ollama' }
] },
{ type: 'ai_custom', label: 'IA Personnalisée', icon: '⚙️', description: 'Appel IA libre avec un system prompt et des entrées personnalisées.', category: 'ai', needsAnchor: false, params: [
{ name: 'prompt', type: 'string', description: 'Prompt utilisateur' },
{ name: 'input_text', type: 'string', description: 'Texte d\'entrée' },
{ name: 'variable_name', type: 'string', description: 'Variable de sortie' },
{ name: 'model', type: 'string', description: 'Modèle Ollama' },
{ name: 'system_prompt', type: 'string', description: 'Prompt système (personnalité, instructions)' }
] },
// === BASE DE DONNÉES ===
{ type: 'db_save_data', label: 'Sauvegarder en BDD', icon: '💿', category: 'data', needsAnchor: false, params: ['table', 'data'] },
{ type: 'db_read_data', label: 'Lire depuis BDD', icon: '📖', category: 'data', needsAnchor: false, params: ['query', 'variable_name'] },
{ type: 'db_save_data', label: 'Sauvegarder en BDD', icon: '💿', description: 'Enregistre des données extraites dans la base de données locale.', category: 'data', needsAnchor: false, params: [
{ name: 'table', type: 'string', description: 'Nom de la table' },
{ name: 'data', type: 'object', description: 'Données à enregistrer (JSON)' }
] },
{ type: 'db_read_data', label: 'Lire depuis BDD', icon: '📖', description: 'Lit des données depuis la base de données locale.', category: 'data', needsAnchor: false, params: [
{ name: 'query', type: 'string', description: 'Requête SQL ou nom de table' },
{ name: 'variable_name', type: 'string', description: 'Variable pour les résultats' }
] },
// === BOUCLE DONNÉES (Data Loop) ===
{ type: 'import_excel', label: 'Importer Excel', icon: '📥', category: 'data', needsAnchor: false, params: ['file_path', 'table_name', 'sheet_name'] },
{ type: 'db_foreach', label: 'Pour chaque ligne', icon: '🔄', category: 'data', needsAnchor: false, params: ['table_name', 'where_clause', 'order_by', 'limit'] },
{ type: 'import_excel', label: 'Importer Excel', icon: '📥', description: 'Importe un fichier Excel/CSV dans la base de données locale.', category: 'data', needsAnchor: false, params: [
{ name: 'file_path', type: 'string', description: 'Chemin du fichier Excel/CSV' },
{ name: 'table_name', type: 'string', description: 'Nom de la table de destination' },
{ name: 'sheet_name', type: 'string', description: 'Nom de la feuille (optionnel)' }
] },
{ type: 'db_foreach', label: 'Pour chaque ligne', icon: '🔄', description: 'Boucle sur chaque ligne d\'une table. Les colonnes deviennent des variables.', category: 'data', needsAnchor: false, params: [
{ name: 'table_name', type: 'string', description: 'Nom de la table source' },
{ name: 'where_clause', type: 'string', description: 'Filtre SQL (optionnel)' },
{ name: 'order_by', type: 'string', description: 'Tri (optionnel)' },
{ name: 'limit', type: 'number', description: 'Nombre max de lignes' }
] },
// === DAG LLM — Actions IA via DAGExecutor (parallèle, Ollama) ===
{ type: 'llm_analyze', label: 'Analyser texte', icon: '🔬', category: 'llm', needsAnchor: false, params: ['text', 'instruction', 'model'] },
{ type: 'llm_translate', label: 'Traduire', icon: '🌐', category: 'llm', needsAnchor: false, params: ['text', 'target_lang', 'model'] },
{ type: 'llm_extract_data', label: 'Extraire données', icon: '🗂️', category: 'llm', needsAnchor: false, params: ['text', 'schema', 'model'] },
{ type: 'llm_generate', label: 'Générer texte', icon: '✍️', category: 'llm', needsAnchor: false, params: ['prompt', 'context', 'model'] },
{ type: 'llm_analyze', label: 'Analyser texte', icon: '🔬', description: 'Analyse un texte avec une instruction via LLM (DAG parallèle).', category: 'llm', needsAnchor: false, params: [
{ name: 'text', type: 'string', description: 'Texte à analyser' },
{ name: 'instruction', type: 'string', description: 'Instruction d\'analyse' },
{ name: 'model', type: 'string', description: 'Modèle Ollama' }
] },
{ type: 'llm_translate', label: 'Traduire', icon: '🌐', description: 'Traduit un texte vers une langue cible via LLM.', category: 'llm', needsAnchor: false, params: [
{ name: 'text', type: 'string', description: 'Texte à traduire' },
{ name: 'target_lang', type: 'string', description: 'Langue cible (fr, en, es, etc.)' },
{ name: 'model', type: 'string', description: 'Modèle Ollama' }
] },
{ type: 'llm_extract_data', label: 'Extraire données', icon: '🗂️', description: 'Extrait des données structurées selon un schéma JSON.', category: 'llm', needsAnchor: false, params: [
{ name: 'text', type: 'string', description: 'Texte source' },
{ name: 'schema', type: 'string', description: 'Schéma JSON des données à extraire' },
{ name: 'model', type: 'string', description: 'Modèle Ollama' }
] },
{ type: 'llm_generate', label: 'Générer texte', icon: '✍️', description: 'Génère du texte libre à partir d\'un prompt et contexte.', category: 'llm', needsAnchor: false, params: [
{ name: 'prompt', type: 'string', description: 'Prompt de génération' },
{ name: 'context', type: 'string', description: 'Contexte additionnel' },
{ name: 'model', type: 'string', description: 'Modèle Ollama' }
] },
// === VALIDATION ===
{ type: 'verify_element_exists', label: 'Vérifier présence', icon: '✅', category: 'validation', needsAnchor: true, params: ['timeout_ms'] },
{ type: 'verify_text_content', label: 'Vérifier texte', icon: '🔍', category: 'validation', needsAnchor: true, params: ['expected_text'] },
{ type: 'verify_element_exists', label: 'Vérifier présence', icon: '✅', description: 'Vérifie qu\'un élément visuel est présent à l\'écran.', category: 'validation', needsAnchor: true, params: [
{ name: 'timeout_ms', type: 'number', description: 'Délai max d\'attente en millisecondes' }
] },
{ type: 'verify_text_content', label: 'Vérifier texte', icon: '🔍', description: 'Vérifie que la zone de l\'ancre contient le texte attendu.', category: 'validation', needsAnchor: true, params: [
{ name: 'expected_text', type: 'string', description: 'Texte attendu dans la zone' }
] },
];
export const ACTION_CATEGORIES = {