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:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user