>({});
+
+ useEffect(() => {
+ if (step) {
+ setParams(step.parameters || {});
+ }
+ }, [step?.id, step?.parameters]);
+
+ if (!step) {
+ return (
+
+
Propriétés
+
Sélectionnez une étape
+
+ );
+ }
+
+ const action = ACTIONS.find(a => a.type === step.action_type);
+
+ const updateParam = (key: string, value: unknown) => {
+ setParams(prev => ({ ...prev, [key]: value }));
+ };
+
+ const handleSave = () => {
+ onUpdateParams(step.id, params);
+ };
+
+ const renderParamsForAction = (actionType: ActionType) => {
+ switch (actionType) {
+ // === SOURIS ===
+ case 'click_anchor':
+ case 'double_click_anchor':
+ case 'right_click_anchor':
+ return (
+ <>
+
+
+ updateParam('delay_before', Number(e.target.value))}
+ min="0"
+ step="100"
+ />
+
+
+
+ updateParam('delay_after', Number(e.target.value))}
+ min="0"
+ step="100"
+ />
+
+ >
+ );
+
+ case 'hover_anchor':
+ return (
+
+
+ updateParam('hover_duration', Number(e.target.value))}
+ min="0"
+ step="100"
+ />
+
+ );
+
+ case 'drag_drop_anchor':
+ return (
+ <>
+
+
+ updateParam('target_anchor_id', e.target.value)}
+ placeholder="ID de l'ancre cible"
+ />
+
+
+
+
+
+ >
+ );
+
+ case 'scroll_to_anchor':
+ return (
+
+
+
+
+ );
+
+ case 'focus_anchor':
+ return (
+
+
+
+
+ );
+
+ // === CLAVIER ===
+ case 'type_text':
+ return (
+ <>
+
+
+
+
+
+ updateParam('typing_speed', Number(e.target.value))}
+ min="0"
+ max="500"
+ step="10"
+ />
+
+
+
+
+ >
+ );
+
+ case 'type_secret':
+ return (
+ <>
+
+
+ updateParam('secret_key', e.target.value)}
+ placeholder="MON_MOT_DE_PASSE"
+ />
+
+
+ Le secret sera lu depuis les variables d'environnement
+
+ >
+ );
+
+ case 'keyboard_shortcut':
+ return (
+ <>
+
+
+ updateParam('keys', e.target.value.split('+').map(k => k.trim()))}
+ placeholder="ctrl+c, alt+tab, enter..."
+ />
+
+
+ Touches disponibles: ctrl, alt, shift, cmd, tab, enter, escape, space, up, down, left, right, f1-f12
+
+ >
+ );
+
+ // === ATTENTE ===
+ case 'wait_for_anchor':
+ return (
+ <>
+
+
+ updateParam('timeout_ms', Number(e.target.value))}
+ min="1000"
+ step="1000"
+ />
+
+
+
+ updateParam('check_interval', Number(e.target.value))}
+ min="100"
+ step="100"
+ />
+
+
+
+
+ >
+ );
+
+ // === DONNÉES ===
+ case 'extract_text':
+ return (
+ <>
+
+
+ updateParam('variable_name', e.target.value)}
+ placeholder="texte_extrait"
+ />
+
+
+
+
+
+
+
+ updateParam('regex_filter', e.target.value)}
+ placeholder="Ex: \d{4}-\d{2}-\d{2}"
+ />
+
+ >
+ );
+
+ case 'extract_table':
+ return (
+ <>
+
+
+ updateParam('variable_name', e.target.value)}
+ placeholder="tableau_extrait"
+ />
+
+
+
+
+
+
+
+
+ >
+ );
+
+ case 'screenshot_evidence':
+ return (
+ <>
+
+
+ updateParam('filename', e.target.value)}
+ placeholder="capture_{timestamp}"
+ />
+
+
+
+
+
+ >
+ );
+
+ case 'download_to_folder':
+ return (
+ <>
+
+
+ updateParam('folder_path', e.target.value)}
+ placeholder="/chemin/vers/dossier"
+ />
+
+
+
+ updateParam('download_timeout', Number(e.target.value))}
+ min="5000"
+ step="5000"
+ />
+
+ >
+ );
+
+ // === LOGIQUE ===
+ case 'visual_condition':
+ return (
+ <>
+
+
+ updateParam('on_found', e.target.value)}
+ placeholder="ID de l'étape"
+ />
+
+
+
+ updateParam('on_not_found', e.target.value)}
+ placeholder="ID de l'étape"
+ />
+
+
+
+ updateParam('search_timeout', Number(e.target.value))}
+ min="1000"
+ step="1000"
+ />
+
+ >
+ );
+
+ case 'loop_visual':
+ return (
+ <>
+
+
+ updateParam('max_iterations', Number(e.target.value))}
+ min="1"
+ max="1000"
+ />
+
+
+
+
+
+
+
+ updateParam('iteration_delay', Number(e.target.value))}
+ min="100"
+ step="100"
+ />
+
+ >
+ );
+
+ // === IA ===
+ case 'ai_analyze_text':
+ return (
+ <>
+
+
+
+
+
+ updateParam('variable_name', e.target.value)}
+ placeholder="resultat_ia"
+ />
+
+
+
+
+
+ >
+ );
+
+ // === BDD ===
+ case 'db_save_data':
+ return (
+ <>
+
+
+ updateParam('table', e.target.value)}
+ placeholder="ma_table"
+ />
+
+
+
+
+ >
+ );
+
+ case 'db_read_data':
+ return (
+ <>
+
+
+
+
+
+ updateParam('variable_name', e.target.value)}
+ placeholder="resultat_requete"
+ />
+
+ >
+ );
+
+ // === VALIDATION ===
+ case 'verify_element_exists':
+ return (
+ <>
+
+
+ updateParam('timeout_ms', Number(e.target.value))}
+ min="1000"
+ step="1000"
+ />
+
+
+
+
+ >
+ );
+
+ case 'verify_text_content':
+ return (
+ <>
+
+
+ updateParam('expected_text', e.target.value)}
+ placeholder="Texte à vérifier"
+ />
+
+
+
+
+
+
+
+
+ >
+ );
+
+ default:
+ return Pas de paramètres supplémentaires
;
+ }
+ };
+
+ return (
+
+
Propriétés
+
+
+ {action?.icon}
+ {action?.label || step.action_type}
+
+
+
ID: {step.id.slice(0, 25)}...
+
+ {/* Paramètres spécifiques à l'action */}
+
+ {renderParamsForAction(step.action_type)}
+
+
+ {/* Ancre visuelle */}
+ {action?.needsAnchor && (
+
+
+ {step.anchor_id ? (
+
+
})
+
✓ Définie
+
+ ) : (
+
+ ⚠ Non définie - Utilisez la capture
+
+ )}
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/visual_workflow_builder/frontend_v4/src/components/StepNode.tsx b/visual_workflow_builder/frontend_v4/src/components/StepNode.tsx
new file mode 100644
index 000000000..5274f4521
--- /dev/null
+++ b/visual_workflow_builder/frontend_v4/src/components/StepNode.tsx
@@ -0,0 +1,83 @@
+import { memo } from 'react';
+import { Handle, Position } from '@xyflow/react';
+import type { Step } from '../types';
+import { ACTIONS } from '../types';
+import { getAnchorThumbnailUrl } from '../services/api';
+
+interface StepNodeProps {
+ data: { step: Step };
+ selected?: boolean;
+}
+
+function StepNode({ data, selected }: StepNodeProps) {
+ const step = data.step;
+ const action = ACTIONS.find(a => a.type === step.action_type);
+ const isConditional = step.action_type === 'visual_condition' || step.action_type === 'loop_visual';
+
+ return (
+
+ {/* Entrée: haut */}
+
+
+
+ {action?.icon || '?'}
+ {action?.label || step.action_type}
+
+
+ {step.anchor_id && (
+
+
})
{ (e.target as HTMLImageElement).style.display = 'none'; }}
+ />
+
+ )}
+
+ {step.action_type === 'type_text' && typeof step.parameters?.text === 'string' && step.parameters.text.length > 0 && (
+
+ {`"${step.parameters.text.slice(0, 20)}${step.parameters.text.length > 20 ? '...' : ''}"`}
+
+ )}
+
+ {!step.anchor_id && action?.needsAnchor && (
+
+ Ancre requise
+
+ )}
+
+ {/* Sortie principale: bas */}
+
+
+ {/* Sortie alternative: droite (pour conditions/boucles) */}
+
+
+ {/* Entrée latérale: gauche (pour retours de boucle) */}
+
+
+ );
+}
+
+export default memo(StepNode);
diff --git a/visual_workflow_builder/frontend_v4/src/components/ToolPalette.tsx b/visual_workflow_builder/frontend_v4/src/components/ToolPalette.tsx
new file mode 100644
index 000000000..ebe76743b
--- /dev/null
+++ b/visual_workflow_builder/frontend_v4/src/components/ToolPalette.tsx
@@ -0,0 +1,49 @@
+import { ACTIONS, ACTION_CATEGORIES } from '../types';
+
+export default function ToolPalette() {
+ const onDragStart = (event: React.DragEvent, actionType: string) => {
+ event.dataTransfer.setData('actionType', actionType);
+ event.dataTransfer.effectAllowed = 'move';
+ };
+
+ // Grouper par catégorie
+ const categories = Object.keys(ACTION_CATEGORIES) as Array;
+
+ return (
+
+
Outils
+
+ {categories.map((catKey) => {
+ const cat = ACTION_CATEGORIES[catKey];
+ const tools = ACTIONS.filter(a => a.category === catKey);
+
+ if (tools.length === 0) return null;
+
+ return (
+
+
+ {cat.icon}
+ {cat.label}
+
+
+ {tools.map((action) => (
+
onDragStart(e, action.type)}
+ title={action.label}
+ >
+ {action.icon}
+ {action.label}
+ {action.needsAnchor && 🎯}
+
+ ))}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/visual_workflow_builder/frontend_v4/src/components/WorkflowList.tsx b/visual_workflow_builder/frontend_v4/src/components/WorkflowList.tsx
new file mode 100644
index 000000000..ebbf1f2e1
--- /dev/null
+++ b/visual_workflow_builder/frontend_v4/src/components/WorkflowList.tsx
@@ -0,0 +1,44 @@
+import type { WorkflowSummary } from '../types';
+
+interface Props {
+ workflows: WorkflowSummary[];
+ activeId: string | null;
+ onSelect: (id: string) => void;
+ onCreate: () => void;
+ onDelete: (id: string) => void;
+}
+
+export default function WorkflowList({ workflows, activeId, onSelect, onCreate, onDelete }: Props) {
+ return (
+
+
+
Workflows
+
+
+
+ {workflows.length === 0 ? (
+
Aucun workflow
+ ) : (
+
+ {workflows.map(wf => (
+ - onSelect(wf.id)}
+ >
+ {wf.name}
+ {wf.step_count} étapes
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/visual_workflow_builder/frontend_v4/src/main.tsx b/visual_workflow_builder/frontend_v4/src/main.tsx
new file mode 100644
index 000000000..6906b28b1
--- /dev/null
+++ b/visual_workflow_builder/frontend_v4/src/main.tsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App'
+import './styles.css'
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/visual_workflow_builder/frontend_v4/src/services/api.ts b/visual_workflow_builder/frontend_v4/src/services/api.ts
new file mode 100644
index 000000000..a3e39fadd
--- /dev/null
+++ b/visual_workflow_builder/frontend_v4/src/services/api.ts
@@ -0,0 +1,152 @@
+/**
+ * API Client - Toutes les interactions avec le backend
+ */
+
+import type { AppState, Workflow, Step, Execution, Capture, ActionType } from '../types';
+
+const API_BASE = '/api/v3';
+
+async function request(method: string, endpoint: string, body?: unknown): Promise {
+ const options: RequestInit = {
+ method,
+ headers: { 'Content-Type': 'application/json' }
+ };
+
+ if (body) {
+ options.body = JSON.stringify(body);
+ }
+
+ const response = await fetch(`${API_BASE}${endpoint}`, options);
+ const data = await response.json();
+
+ if (!data.success) {
+ throw new Error(data.error || 'Erreur API');
+ }
+
+ return data;
+}
+
+// Session
+export async function getState(): Promise {
+ const data = await request<{
+ success: boolean;
+ session: AppState['session'];
+ workflow: AppState['workflow'];
+ execution: AppState['execution'];
+ workflows_list: AppState['workflows_list'];
+ }>('GET', '/session/state');
+
+ return {
+ session: data.session,
+ workflow: data.workflow,
+ execution: data.execution,
+ workflows_list: data.workflows_list
+ };
+}
+
+export async function selectWorkflow(workflowId: string): Promise<{ session: AppState['session']; workflow: Workflow }> {
+ return request('POST', `/session/select-workflow/${workflowId}`);
+}
+
+export async function selectStep(stepId: string): Promise<{ session: AppState['session']; step: Step }> {
+ return request('POST', `/session/select-step/${stepId}`);
+}
+
+// Workflow CRUD
+export async function createWorkflow(name: string, description?: string): Promise<{ workflow: Workflow; session: AppState['session'] }> {
+ return request('POST', '/workflow/create', { name, description });
+}
+
+export async function deleteWorkflow(workflowId: string): Promise<{ deleted_id: string; session: AppState['session'] }> {
+ return request('DELETE', `/workflow/${workflowId}`);
+}
+
+// Steps
+export async function addStep(
+ workflowId: string,
+ actionType: ActionType,
+ options?: {
+ position?: { x: number; y: number };
+ parameters?: Record;
+ label?: string;
+ insertAfter?: string;
+ }
+): Promise<{ workflow: Workflow; step: Step; needs_anchor: boolean; session: AppState['session'] }> {
+ return request('POST', `/workflow/${workflowId}/step`, {
+ action_type: actionType,
+ position: options?.position,
+ parameters: options?.parameters,
+ label: options?.label,
+ insert_after: options?.insertAfter
+ });
+}
+
+export async function updateStep(
+ workflowId: string,
+ stepId: string,
+ updates: {
+ action_type?: string;
+ position?: { x: number; y: number };
+ parameters?: Record;
+ label?: string;
+ }
+): Promise<{ workflow: Workflow; step: Step }> {
+ return request('PUT', `/workflow/${workflowId}/step/${stepId}`, updates);
+}
+
+export async function deleteStep(workflowId: string, stepId: string): Promise<{ workflow: Workflow; session: AppState['session'] }> {
+ return request('DELETE', `/workflow/${workflowId}/step/${stepId}`);
+}
+
+export async function reorderSteps(workflowId: string, stepIds: string[]): Promise<{ workflow: Workflow }> {
+ return request('POST', `/workflow/${workflowId}/reorder`, { step_ids: stepIds });
+}
+
+// Capture
+export async function captureScreen(): Promise<{ capture: Capture }> {
+ return request('POST', '/capture/screen');
+}
+
+export async function selectAnchor(
+ stepId: string,
+ bbox: { x: number; y: number; width: number; height: number },
+ description?: string,
+ screenshotBase64?: string
+): Promise<{ workflow: Workflow; step: Step; anchor: unknown }> {
+ return request('POST', '/capture/select', {
+ step_id: stepId,
+ bbox,
+ description,
+ screenshot_base64: screenshotBase64
+ });
+}
+
+export function getAnchorThumbnailUrl(anchorId: string): string {
+ return `${API_BASE}/anchor/${anchorId}/thumbnail`;
+}
+
+// Execution
+export async function startExecution(workflowId?: string): Promise<{ execution: Execution; session: AppState['session'] }> {
+ return request('POST', '/execute/start', workflowId ? { workflow_id: workflowId } : {});
+}
+
+export async function pauseExecution(): Promise<{ execution: Execution }> {
+ return request('POST', '/execute/pause');
+}
+
+export async function resumeExecution(): Promise<{ execution: Execution }> {
+ return request('POST', '/execute/resume');
+}
+
+export async function stopExecution(): Promise<{ execution: Execution; session: AppState['session'] }> {
+ return request('POST', '/execute/stop');
+}
+
+export async function getExecutionStatus(): Promise<{
+ is_running: boolean;
+ is_paused: boolean;
+ execution: Execution | null;
+ session: AppState['session'];
+}> {
+ return request('GET', '/execute/status');
+}
diff --git a/visual_workflow_builder/frontend_v4/src/styles.css b/visual_workflow_builder/frontend_v4/src/styles.css
new file mode 100644
index 000000000..83aa99a37
--- /dev/null
+++ b/visual_workflow_builder/frontend_v4/src/styles.css
@@ -0,0 +1,742 @@
+/* Reset et base */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: #1a1a2e;
+ color: #eee;
+}
+
+/* Layout principal */
+.app {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.75rem 1rem;
+ background: #16213e;
+ border-bottom: 1px solid #0f3460;
+}
+
+.header h1 {
+ font-size: 1.25rem;
+ color: #e94560;
+}
+
+.error-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.5rem 1rem;
+ background: #e94560;
+ color: white;
+}
+
+.error-bar button {
+ background: none;
+ border: none;
+ color: white;
+ font-size: 1.25rem;
+ cursor: pointer;
+}
+
+.main-layout {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+}
+
+/* Sidebars */
+.sidebar {
+ width: 280px;
+ background: #16213e;
+ border-right: 1px solid #0f3460;
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+}
+
+.sidebar.right {
+ border-right: none;
+ border-left: 1px solid #0f3460;
+}
+
+.sidebar h3, .sidebar h4 {
+ padding: 0.75rem 1rem;
+ background: #0f3460;
+ color: #e94560;
+ font-size: 0.9rem;
+}
+
+/* Canvas central */
+.canvas-container {
+ flex: 1;
+ background: #1a1a2e;
+}
+
+.empty-canvas {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ color: #666;
+}
+
+/* Workflow List */
+.workflow-list-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.75rem 1rem;
+ background: #0f3460;
+}
+
+.workflow-list-header button {
+ background: #e94560;
+ border: none;
+ color: white;
+ width: 28px;
+ height: 28px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 1.25rem;
+}
+
+.workflow-list ul {
+ list-style: none;
+}
+
+.workflow-list li {
+ display: flex;
+ align-items: center;
+ padding: 0.75rem 1rem;
+ cursor: pointer;
+ border-bottom: 1px solid #0f3460;
+}
+
+.workflow-list li:hover {
+ background: #0f3460;
+}
+
+.workflow-list li.active {
+ background: #e94560;
+}
+
+.workflow-list .wf-name {
+ flex: 1;
+}
+
+.workflow-list .wf-count {
+ font-size: 0.75rem;
+ color: #888;
+ margin-right: 0.5rem;
+}
+
+.workflow-list .delete-btn {
+ background: none;
+ border: none;
+ color: #e94560;
+ cursor: pointer;
+ font-size: 1.25rem;
+ opacity: 0.6;
+}
+
+.workflow-list li.active .delete-btn {
+ color: white;
+}
+
+/* Tool Palette */
+.tool-palette {
+ border-top: 1px solid #0f3460;
+ flex: 1;
+ overflow-y: auto;
+}
+
+.tool-categories {
+ padding: 0.5rem;
+}
+
+.tool-category {
+ margin-bottom: 0.75rem;
+}
+
+.category-header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.4rem 0.5rem;
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: #e94560;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ border-bottom: 1px solid #0f3460;
+ margin-bottom: 0.4rem;
+}
+
+.category-icon {
+ font-size: 0.9rem;
+}
+
+.category-label {
+ flex: 1;
+}
+
+.tool-list {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.tool-item {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ padding: 0.5rem 0.6rem;
+ background: #0f3460;
+ border-radius: 4px;
+ cursor: grab;
+ transition: all 0.15s;
+ font-size: 0.8rem;
+}
+
+.tool-item:hover {
+ background: #e94560;
+ transform: translateX(2px);
+}
+
+.tool-item:active {
+ cursor: grabbing;
+}
+
+.tool-icon {
+ font-size: 1rem;
+ width: 20px;
+ text-align: center;
+}
+
+.tool-label {
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.tool-anchor-badge {
+ font-size: 0.7rem;
+ opacity: 0.6;
+}
+
+/* Step Node (React Flow) */
+.step-node {
+ background: #16213e;
+ border: 2px solid #0f3460;
+ border-radius: 8px;
+ width: 160px;
+ padding: 0.5rem;
+ font-size: 12px;
+}
+
+.step-node.selected {
+ border-color: #e94560;
+ box-shadow: 0 0 12px rgba(233, 69, 96, 0.5);
+}
+
+.step-node-header {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ padding-bottom: 0.4rem;
+ border-bottom: 1px solid #0f3460;
+}
+
+.step-icon {
+ font-size: 1.1rem;
+}
+
+.step-label {
+ font-weight: 600;
+ font-size: 0.8rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.step-node-anchor {
+ margin-top: 0.4rem;
+ background: #0f3460;
+ border-radius: 4px;
+ padding: 3px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.step-node-anchor img {
+ width: 100%;
+ height: 40px;
+ max-width: 140px;
+ object-fit: contain;
+ border-radius: 3px;
+ background: #1a1a2e;
+}
+
+.step-node-params {
+ margin-top: 0.4rem;
+ padding: 0.3rem;
+ background: #0f3460;
+ border-radius: 3px;
+ font-size: 0.7rem;
+ color: #aaa;
+ font-style: italic;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.step-node-warning {
+ margin-top: 0.4rem;
+ padding: 0.25rem;
+ font-size: 0.7rem;
+ color: #e94560;
+ text-align: center;
+ background: rgba(233, 69, 96, 0.1);
+ border-radius: 3px;
+}
+
+/* Nœud conditionnel */
+.step-node.conditional {
+ border-color: #ff9800;
+}
+
+.step-node.conditional.selected {
+ border-color: #ffb74d;
+ box-shadow: 0 0 12px rgba(255, 152, 0, 0.5);
+}
+
+/* Handles (points d'accroche) */
+.step-node .react-flow__handle {
+ width: 10px;
+ height: 10px;
+ border: 2px solid #0f3460;
+ background: #16213e;
+ transition: all 0.15s;
+}
+
+.step-node .react-flow__handle:hover {
+ background: #e94560;
+ border-color: #e94560;
+ transform: scale(1.3);
+}
+
+/* Handle haut - entrée principale */
+.step-node .handle-top {
+ background: #4caf50;
+ border-color: #4caf50;
+}
+
+/* Handle bas - sortie principale */
+.step-node .handle-bottom {
+ background: #2196f3;
+ border-color: #2196f3;
+}
+
+/* Handle droite - sortie alternative (conditions) */
+.step-node .handle-right {
+ background: #ff9800;
+ border-color: #ff9800;
+ width: 8px;
+ height: 8px;
+}
+
+/* Handle gauche - entrée secondaire (retour boucle) */
+.step-node .handle-left {
+ background: #9c27b0;
+ border-color: #9c27b0;
+ width: 8px;
+ height: 8px;
+}
+
+/* Properties Panel */
+.properties-panel {
+ padding: 1rem;
+}
+
+.properties-panel .empty {
+ color: #666;
+ font-style: italic;
+}
+
+.prop-header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.prop-icon {
+ font-size: 1.5rem;
+}
+
+.prop-type {
+ font-size: 1.1rem;
+ font-weight: 500;
+}
+
+.prop-id {
+ font-size: 0.7rem;
+ color: #666;
+ margin-bottom: 1rem;
+}
+
+.prop-field {
+ margin-bottom: 1rem;
+}
+
+.prop-field label {
+ display: block;
+ margin-bottom: 0.25rem;
+ font-size: 0.85rem;
+ color: #aaa;
+}
+
+.prop-field input,
+.prop-field textarea {
+ width: 100%;
+ padding: 0.5rem;
+ background: #0f3460;
+ border: 1px solid #1a1a2e;
+ color: #eee;
+ border-radius: 4px;
+}
+
+.prop-field textarea {
+ resize: vertical;
+}
+
+.prop-anchor {
+ margin-bottom: 1rem;
+}
+
+.prop-anchor label {
+ display: block;
+ margin-bottom: 0.5rem;
+ font-size: 0.85rem;
+ color: #aaa;
+}
+
+.anchor-preview {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.anchor-preview img {
+ width: 80px;
+ height: 50px;
+ object-fit: cover;
+ border-radius: 4px;
+ border: 1px solid #0f3460;
+}
+
+.anchor-ok {
+ color: #4caf50;
+ font-size: 0.85rem;
+}
+
+.anchor-missing {
+ color: #e94560;
+ font-size: 0.85rem;
+}
+
+.prop-actions {
+ display: flex;
+ gap: 0.5rem;
+ margin-top: 1rem;
+}
+
+.prop-actions button {
+ flex: 1;
+ padding: 0.5rem;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+.btn-save {
+ background: #4caf50;
+ color: white;
+}
+
+.btn-delete {
+ background: #e94560;
+ color: white;
+}
+
+/* Capture Panel */
+.capture-panel {
+ padding: 1rem;
+ border-top: 1px solid #0f3460;
+}
+
+.capture-controls {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.capture-controls button,
+.capture-controls select {
+ padding: 0.5rem;
+ background: #0f3460;
+ border: none;
+ color: #eee;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+.capture-controls button:hover {
+ background: #e94560;
+}
+
+.capture-preview {
+ margin-bottom: 1rem;
+}
+
+.capture-preview img {
+ width: 100%;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+.capture-info {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 0.75rem;
+ color: #888;
+ margin-top: 0.25rem;
+}
+
+.capture-info button {
+ font-size: 0.75rem;
+ padding: 0.25rem 0.5rem;
+ background: #0f3460;
+ border: none;
+ color: #eee;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+.capture-hint {
+ font-size: 0.8rem;
+ color: #e94560;
+ font-style: italic;
+ margin-bottom: 1rem;
+}
+
+/* Library */
+.capture-library h4 {
+ font-size: 0.85rem;
+ margin-bottom: 0.5rem;
+ padding: 0;
+ background: none;
+}
+
+.library-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 0.5rem;
+}
+
+.library-item {
+ position: relative;
+}
+
+.library-item img {
+ width: 100%;
+ height: 50px;
+ object-fit: cover;
+ border-radius: 4px;
+ cursor: pointer;
+ border: 2px solid transparent;
+}
+
+.library-item img:hover {
+ border-color: #e94560;
+}
+
+.library-item .delete-btn {
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ background: rgba(233, 69, 96, 0.8);
+ border: none;
+ color: white;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ cursor: pointer;
+ font-size: 0.75rem;
+ display: none;
+}
+
+.library-item:hover .delete-btn {
+ display: block;
+}
+
+/* Fullscreen Modal */
+.fullscreen-modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.95);
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+}
+
+.fullscreen-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem;
+ background: #16213e;
+}
+
+.fullscreen-header button {
+ padding: 0.5rem 1rem;
+ background: #e94560;
+ border: none;
+ color: white;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+.fullscreen-content {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: auto;
+ position: relative;
+}
+
+.fullscreen-content img {
+ max-width: 100%;
+ max-height: 100%;
+ cursor: crosshair;
+}
+
+.selection-overlay {
+ position: absolute;
+ border: 2px solid #e94560;
+ background: rgba(233, 69, 96, 0.2);
+ pointer-events: none;
+}
+
+/* Execution Controls */
+.execution-controls {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.btn-start, .btn-stop {
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: 500;
+}
+
+.btn-start {
+ background: #4caf50;
+ color: white;
+}
+
+.btn-stop {
+ background: #e94560;
+ color: white;
+}
+
+.exec-status {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.status-badge {
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.8rem;
+}
+
+.status-badge.running {
+ background: #4caf50;
+ color: white;
+}
+
+.status-badge.paused {
+ background: #ff9800;
+ color: white;
+}
+
+.status-badge.completed {
+ background: #4caf50;
+ color: white;
+}
+
+.status-badge.error {
+ background: #e94560;
+ color: white;
+}
+
+.exec-progress {
+ font-size: 0.85rem;
+ color: #aaa;
+}
+
+/* React Flow overrides */
+.react-flow__node {
+ font-size: 12px;
+}
+
+.react-flow__edge-path {
+ stroke: #0f3460;
+ stroke-width: 2;
+}
+
+.react-flow__edge.selected .react-flow__edge-path {
+ stroke: #e94560;
+}
+
+.react-flow__controls {
+ background: #16213e;
+ border: 1px solid #0f3460;
+}
+
+.react-flow__controls button {
+ background: #16213e;
+ border-bottom: 1px solid #0f3460;
+ color: #eee;
+}
+
+.react-flow__controls button:hover {
+ background: #0f3460;
+}
+
+.react-flow__background {
+ background: #1a1a2e;
+}
diff --git a/visual_workflow_builder/frontend_v4/src/types.ts b/visual_workflow_builder/frontend_v4/src/types.ts
new file mode 100644
index 000000000..934e86f34
--- /dev/null
+++ b/visual_workflow_builder/frontend_v4/src/types.ts
@@ -0,0 +1,149 @@
+// Types pour l'API v3
+
+export type ActionType =
+ | 'click_anchor'
+ | 'double_click_anchor'
+ | 'right_click_anchor'
+ | 'hover_anchor'
+ | 'type_text'
+ | 'type_secret'
+ | 'focus_anchor'
+ | 'wait_for_anchor'
+ | 'scroll_to_anchor'
+ | 'drag_drop_anchor'
+ | 'keyboard_shortcut'
+ | 'extract_text'
+ | 'extract_table'
+ | 'screenshot_evidence'
+ | 'visual_condition'
+ | 'loop_visual'
+ | 'download_to_folder'
+ | 'ai_analyze_text'
+ | 'db_save_data'
+ | 'db_read_data'
+ | 'verify_element_exists'
+ | 'verify_text_content';
+
+export interface ActionDefinition {
+ type: ActionType;
+ label: string;
+ icon: string;
+ category: 'mouse' | 'keyboard' | 'wait' | 'data' | 'logic' | 'ai' | 'validation';
+ needsAnchor: boolean;
+ params: 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: [] },
+
+ // === 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'] },
+
+ // === ATTENTE ===
+ { type: 'wait_for_anchor', label: 'Attendre élément', icon: '⏳', category: 'wait', needsAnchor: true, params: ['timeout_ms'] },
+
+ // === 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'] },
+
+ // === 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'] },
+
+ // === INTELLIGENCE ARTIFICIELLE ===
+ { type: 'ai_analyze_text', label: 'Analyse IA', icon: '🤖', category: 'ai', needsAnchor: true, params: ['prompt', 'variable_name'] },
+
+ // === 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'] },
+
+ // === 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'] },
+];
+
+export const ACTION_CATEGORIES = {
+ mouse: { label: 'Souris', icon: '🖱️' },
+ keyboard: { label: 'Clavier', icon: '⌨️' },
+ wait: { label: 'Attente', icon: '⏳' },
+ data: { label: 'Données', icon: '📊' },
+ logic: { label: 'Logique', icon: '🔀' },
+ ai: { label: 'IA', icon: '🤖' },
+ validation: { label: 'Validation', icon: '✅' },
+};
+
+export interface VisualAnchor {
+ id: string;
+ bounding_box: { x: number; y: number; width: number; height: number };
+ thumbnail_url?: string;
+ description?: string;
+}
+
+export interface Step {
+ id: string;
+ action_type: ActionType;
+ order: number;
+ label: string;
+ position: { x: number; y: number };
+ parameters: Record;
+ anchor_id?: string;
+ anchor?: VisualAnchor;
+}
+
+export interface Workflow {
+ id: string;
+ name: string;
+ description?: string;
+ steps: Step[];
+ created_at: string;
+ updated_at: string;
+}
+
+export interface WorkflowSummary {
+ id: string;
+ name: string;
+ step_count: number;
+}
+
+export interface Execution {
+ id: string;
+ workflow_id: string;
+ status: 'pending' | 'running' | 'paused' | 'completed' | 'error' | 'cancelled';
+ progress: number;
+ current_step_index: number;
+ completed_steps: number;
+ failed_steps: number;
+ total_steps: number;
+ error_message?: string;
+}
+
+export interface Session {
+ active_workflow_id: string | null;
+ selected_step_id: string | null;
+ active_execution_id: string | null;
+}
+
+export interface Capture {
+ screenshot_base64: string;
+ width: number;
+ height: number;
+ timestamp: string;
+}
+
+export interface AppState {
+ session: Session;
+ workflow: Workflow | null;
+ execution: Execution | null;
+ workflows_list: WorkflowSummary[];
+}
diff --git a/visual_workflow_builder/frontend_v4/tsconfig.json b/visual_workflow_builder/frontend_v4/tsconfig.json
new file mode 100644
index 000000000..17f43b17a
--- /dev/null
+++ b/visual_workflow_builder/frontend_v4/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/visual_workflow_builder/frontend_v4/tsconfig.node.json b/visual_workflow_builder/frontend_v4/tsconfig.node.json
new file mode 100644
index 000000000..42872c59f
--- /dev/null
+++ b/visual_workflow_builder/frontend_v4/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/visual_workflow_builder/frontend_v4/vite.config.ts b/visual_workflow_builder/frontend_v4/vite.config.ts
new file mode 100644
index 000000000..3119b8d20
--- /dev/null
+++ b/visual_workflow_builder/frontend_v4/vite.config.ts
@@ -0,0 +1,14 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:5001',
+ changeOrigin: true
+ }
+ }
+ }
+})