feat(vwb): load palette from catalog
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import type { ExecutionMode } from '../types';
|
||||
|
||||
interface StepScore {
|
||||
stepIndex: number;
|
||||
@@ -21,7 +22,7 @@ interface StepScore {
|
||||
|
||||
interface Props {
|
||||
isExecutionRunning: boolean;
|
||||
executionMode: 'basic' | 'intelligent' | 'debug';
|
||||
executionMode: ExecutionMode;
|
||||
}
|
||||
|
||||
export default function ConfidenceDashboard({ isExecutionRunning, executionMode }: Props) {
|
||||
|
||||
@@ -343,6 +343,98 @@ export default function PropertiesPanel({ step, onUpdateParams, onDelete }: Prop
|
||||
</>
|
||||
);
|
||||
|
||||
case 'wait_for_state': {
|
||||
const expectedState = (
|
||||
params.expected_state &&
|
||||
typeof params.expected_state === 'object' &&
|
||||
!Array.isArray(params.expected_state)
|
||||
) ? params.expected_state as Record<string, unknown> : {};
|
||||
|
||||
const updateExpectedState = (key: string, value: unknown) => {
|
||||
updateParam('expected_state', {
|
||||
...expectedState,
|
||||
[key]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const titleIn = Array.isArray(expectedState.window_title_in)
|
||||
? expectedState.window_title_in.join(', ')
|
||||
: '';
|
||||
const titleContains = Array.isArray(expectedState.window_title_contains)
|
||||
? expectedState.window_title_contains.join(', ')
|
||||
: '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="prop-field">
|
||||
<label>Titres exacts acceptés</label>
|
||||
<input
|
||||
type="text"
|
||||
value={titleIn}
|
||||
onChange={(e) => updateExpectedState(
|
||||
'window_title_in',
|
||||
e.target.value.split(',').map(v => v.trim()).filter(Boolean),
|
||||
)}
|
||||
placeholder="Exécuter, Enregistrer sous"
|
||||
/>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Titre contient</label>
|
||||
<input
|
||||
type="text"
|
||||
value={titleContains}
|
||||
onChange={(e) => updateExpectedState(
|
||||
'window_title_contains',
|
||||
e.target.value.split(',').map(v => v.trim()).filter(Boolean),
|
||||
)}
|
||||
placeholder="Bloc-notes"
|
||||
/>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Processus actif</label>
|
||||
<input
|
||||
type="text"
|
||||
value={String(expectedState.process_active || '')}
|
||||
onChange={(e) => updateExpectedState('process_active', e.target.value)}
|
||||
placeholder="explorer.exe"
|
||||
/>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Timeout (ms)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={Number(params.timeout_ms || 5000)}
|
||||
onChange={(e) => updateParam('timeout_ms', Number(e.target.value))}
|
||||
min="100"
|
||||
step="250"
|
||||
/>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Intervalle de vérification (ms)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={Number(params.poll_interval_ms || 250)}
|
||||
onChange={(e) => updateParam('poll_interval_ms', Number(e.target.value))}
|
||||
min="50"
|
||||
step="50"
|
||||
/>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Preuve requise</label>
|
||||
<select
|
||||
value={String(params.evidence_required || 'window_or_process')}
|
||||
onChange={(e) => updateParam('evidence_required', e.target.value)}
|
||||
>
|
||||
<option value="window_or_process">Fenêtre ou process</option>
|
||||
<option value="uia">UIA</option>
|
||||
<option value="ocr">OCR</option>
|
||||
<option value="screenshot_diff">Différence écran</option>
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// === DONNÉES ===
|
||||
case 'extract_text':
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,90 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { ACTIONS, ACTION_CATEGORIES } from '../types';
|
||||
import type { ActionDefinition, ActionType } from '../types';
|
||||
|
||||
interface CatalogActionParameter {
|
||||
type?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface CatalogAction {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
icon?: string;
|
||||
parameters?: Record<string, CatalogActionParameter>;
|
||||
}
|
||||
|
||||
const ACTION_TYPE_SET = new Set(ACTIONS.map(action => action.type));
|
||||
|
||||
function mapCatalogCategory(action: CatalogAction): ActionDefinition['category'] {
|
||||
if (action.category === 'keyboard') return 'keyboard';
|
||||
if (action.category === 'wait' || action.category === 'control') return 'wait';
|
||||
if (action.category === 'logic') return 'logic';
|
||||
if (action.category === 'data') return 'data';
|
||||
if (action.id === 'type_text' || action.id === 'type_secret') return 'keyboard';
|
||||
return 'mouse';
|
||||
}
|
||||
|
||||
function needsAnchor(action: CatalogAction): boolean {
|
||||
const params = action.parameters || {};
|
||||
return Boolean(params.visual_anchor);
|
||||
}
|
||||
|
||||
function mapCatalogAction(action: CatalogAction): ActionDefinition | null {
|
||||
if (!ACTION_TYPE_SET.has(action.id as ActionType)) {
|
||||
return null;
|
||||
}
|
||||
const staticAction = ACTIONS.find(item => item.type === action.id);
|
||||
const catalogParams = Object.entries(action.parameters || {}).map(([name, param]) => ({
|
||||
name,
|
||||
type: String(param.type || 'string'),
|
||||
description: String(param.description || ''),
|
||||
}));
|
||||
return {
|
||||
type: action.id as ActionType,
|
||||
label: action.name || staticAction?.label || action.id,
|
||||
icon: action.icon || staticAction?.icon || '⚙️',
|
||||
description: action.description || staticAction?.description || '',
|
||||
category: staticAction?.category || mapCatalogCategory(action),
|
||||
needsAnchor: staticAction?.needsAnchor ?? needsAnchor(action),
|
||||
params: staticAction?.params.length ? staticAction.params : catalogParams,
|
||||
};
|
||||
}
|
||||
|
||||
export default function ToolPalette() {
|
||||
// État des catégories dépliées (toutes fermées par défaut sauf 'mouse')
|
||||
const [expandedCategories, setExpandedCategories] = useState<string[]>(['mouse']);
|
||||
const [catalogActions, setCatalogActions] = useState<ActionDefinition[]>(ACTIONS);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function loadCatalog() {
|
||||
try {
|
||||
const response = await fetch('/api/vwb/catalog/actions');
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
if (!data.success || !Array.isArray(data.actions)) {
|
||||
throw new Error(data.error || 'Catalogue VWB invalide');
|
||||
}
|
||||
const mapped = data.actions
|
||||
.map((action: CatalogAction) => mapCatalogAction(action))
|
||||
.filter((action: ActionDefinition | null): action is ActionDefinition => action !== null);
|
||||
if (!cancelled && mapped.length > 0) {
|
||||
setCatalogActions(mapped);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Catalogue VWB indisponible, palette statique utilisée:', error);
|
||||
}
|
||||
}
|
||||
|
||||
loadCatalog();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onDragStart = (event: React.DragEvent, actionType: string) => {
|
||||
event.dataTransfer.setData('actionType', actionType);
|
||||
@@ -20,6 +101,10 @@ export default function ToolPalette() {
|
||||
|
||||
// Grouper par catégorie
|
||||
const categories = Object.keys(ACTION_CATEGORIES) as Array<keyof typeof ACTION_CATEGORIES>;
|
||||
const visibleActions = useMemo(
|
||||
() => catalogActions.filter(action => !action.hidden),
|
||||
[catalogActions],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="tool-palette">
|
||||
@@ -27,7 +112,7 @@ export default function ToolPalette() {
|
||||
<div className="tool-categories">
|
||||
{categories.map((catKey) => {
|
||||
const cat = ACTION_CATEGORIES[catKey];
|
||||
const tools = ACTIONS.filter(a => a.category === catKey && !a.hidden);
|
||||
const tools = visibleActions.filter(a => a.category === catKey);
|
||||
const isExpanded = expandedCategories.includes(catKey);
|
||||
|
||||
if (tools.length === 0) return null;
|
||||
|
||||
@@ -46,6 +46,7 @@ export type ActionType =
|
||||
| 'type_secret'
|
||||
| 'focus_anchor'
|
||||
| 'wait_for_anchor'
|
||||
| 'wait_for_state'
|
||||
| 'scroll_to_anchor'
|
||||
| 'drag_drop_anchor'
|
||||
| 'keyboard_shortcut'
|
||||
@@ -120,6 +121,11 @@ export const ACTIONS: ActionDefinition[] = [
|
||||
{ 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' }
|
||||
] },
|
||||
{ type: 'wait_for_state', label: 'Attendre état', icon: '⏱️', description: 'Attend un état d\'écran attendu, comme une fenêtre active ou un processus cible.', category: 'wait', needsAnchor: false, params: [
|
||||
{ name: 'expected_state', type: 'object', description: 'Critères attendus (window_title_in, window_title_contains, process_active)' },
|
||||
{ name: 'timeout_ms', type: 'number', description: 'Délai max d\'attente en millisecondes' },
|
||||
{ name: 'poll_interval_ms', type: 'number', description: 'Intervalle de vérification en millisecondes' }
|
||||
] },
|
||||
|
||||
// === EXTRACTION DE DONNÉES ===
|
||||
{ type: 'extract_text', label: 'Extraire texte (OCR écran)', icon: '📋', description: 'OCR EasyOCR fr+en sur le dernier screenshot. Stocke le texte dans une variable réutilisable plus loin via {{output_var}}. Pas d\'ancre nécessaire — extrait toute la page visible.', category: 'data', needsAnchor: false, params: [
|
||||
|
||||
Reference in New Issue
Block a user