feat: VWB panneau droit réorganisé en 3 onglets + galerie bibliothèque
- 3 onglets : Propriétés / Capture / Données - Panneau extensible 320px → 480px au clic - Galerie bibliothèque plein écran - Fix port détection UI : 5001 → 5002 - Boutons aide (?) et supprimer (×) toujours visibles sur les nœuds Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,19 +14,16 @@ import '@xyflow/react/dist/style.css';
|
||||
|
||||
import * as api from './services/api';
|
||||
import type { AppState, Step, ActionType, Capture, ExecutionMode } from './types';
|
||||
import { ACTIONS, EXECUTION_MODES } from './types';
|
||||
// Types importés via les sous-composants
|
||||
import StepNode from './components/StepNode';
|
||||
import ToolPalette from './components/ToolPalette';
|
||||
import PropertiesPanel from './components/PropertiesPanel';
|
||||
import CapturePanel from './components/CapturePanel';
|
||||
import WorkflowSelector from './components/WorkflowSelector';
|
||||
import WorkflowManagerModal from './components/WorkflowManagerModal';
|
||||
import ExecutionControls from './components/ExecutionControls';
|
||||
import ExecutionModeToggle from './components/ExecutionModeToggle';
|
||||
import ExecutionOverlay from './components/ExecutionOverlay';
|
||||
import VariableManager from './components/VariableManager';
|
||||
import type { Variable } from './components/VariableManager';
|
||||
import CaptureLibrary from './components/CaptureLibrary';
|
||||
import RightPanel from './components/RightPanel';
|
||||
import SelfHealingDialog from './components/SelfHealingDialog';
|
||||
import ConfidenceDashboard from './components/ConfidenceDashboard';
|
||||
import WorkflowValidation from './components/WorkflowValidation';
|
||||
@@ -494,42 +491,27 @@ function App() {
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Sidebar droite: Propriétés + Capture + Variables */}
|
||||
<aside className="sidebar right">
|
||||
<PropertiesPanel
|
||||
step={selectedStep || null}
|
||||
onUpdateParams={handleUpdateStepParams}
|
||||
onDelete={handleDeleteStep}
|
||||
/>
|
||||
<CapturePanel
|
||||
capture={capture}
|
||||
onCapture={handleCapture}
|
||||
onSelectAnchor={handleSelectAnchor}
|
||||
hasSelectedStep={!!appState?.session.selected_step_id}
|
||||
executionMode={executionMode}
|
||||
detectionZone={detectionZone}
|
||||
onSetDetectionZone={setDetectionZone}
|
||||
/>
|
||||
<CaptureLibrary
|
||||
currentCapture={currentCapture}
|
||||
onSelectCapture={handleSelectCaptureFromLibrary}
|
||||
onCapture={handleCapture}
|
||||
/>
|
||||
<VariableManager
|
||||
variables={variables}
|
||||
onVariableCreate={handleVariableCreate}
|
||||
onVariableUpdate={handleVariableUpdate}
|
||||
onVariableDelete={handleVariableDelete}
|
||||
steps={appState?.workflow?.steps || []}
|
||||
runtimeVariables={runtimeVariables}
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{/* Indicateur de mode flottant */}
|
||||
<div className={`mode-indicator ${executionMode}`}>
|
||||
<span>{EXECUTION_MODES[executionMode].icon}</span>
|
||||
<span>Mode {EXECUTION_MODES[executionMode].label}</span>
|
||||
{/* Sidebar droite: Panneau à onglets (Propriétés / Capture / Données) */}
|
||||
<RightPanel
|
||||
selectedStep={selectedStep || null}
|
||||
onUpdateStepParams={handleUpdateStepParams}
|
||||
onDeleteStep={handleDeleteStep}
|
||||
capture={capture}
|
||||
onCapture={handleCapture}
|
||||
onSelectAnchor={handleSelectAnchor}
|
||||
hasSelectedStep={!!appState?.session.selected_step_id}
|
||||
executionMode={executionMode}
|
||||
detectionZone={detectionZone}
|
||||
onSetDetectionZone={setDetectionZone}
|
||||
currentCapture={currentCapture}
|
||||
onSelectCaptureFromLibrary={handleSelectCaptureFromLibrary}
|
||||
variables={variables}
|
||||
onVariableCreate={handleVariableCreate}
|
||||
onVariableUpdate={handleVariableUpdate}
|
||||
onVariableDelete={handleVariableDelete}
|
||||
steps={appState?.workflow?.steps || []}
|
||||
runtimeVariables={runtimeVariables}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Overlay de debug en temps réel */}
|
||||
|
||||
@@ -35,6 +35,7 @@ export default function CapturePanel({
|
||||
onSetDetectionZone
|
||||
}: Props) {
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [showLibraryGallery, setShowLibraryGallery] = useState(false);
|
||||
const [library, setLibrary] = useState<LibraryItem[]>([]);
|
||||
const [currentCapture, setCurrentCapture] = useState<Capture | null>(null);
|
||||
const [timerSeconds, setTimerSeconds] = useState(0);
|
||||
@@ -133,6 +134,7 @@ export default function CapturePanel({
|
||||
|
||||
const handleLibrarySelect = (item: LibraryItem) => {
|
||||
setCurrentCapture(item.capture);
|
||||
setIsFullscreen(true); // Ouvrir en plein écran pour sélectionner
|
||||
};
|
||||
|
||||
const handleDeleteLibraryItem = (id: string) => {
|
||||
@@ -220,9 +222,12 @@ export default function CapturePanel({
|
||||
|
||||
{/* Bibliothèque */}
|
||||
<div className="capture-library">
|
||||
<h4>Bibliothèque ({library.length})</h4>
|
||||
<h4 style={{ cursor: 'pointer' }} onClick={() => library.length > 0 && setShowLibraryGallery(true)}>
|
||||
Bibliothèque ({library.length})
|
||||
{library.length > 0 && <span style={{ fontSize: '11px', marginLeft: '8px', color: 'var(--primary)' }}>▸ Ouvrir</span>}
|
||||
</h4>
|
||||
<div className="library-grid">
|
||||
{library.map(item => (
|
||||
{library.slice(0, 4).map(item => (
|
||||
<div key={item.id} className="library-item">
|
||||
<img
|
||||
src={`data:image/png;base64,${item.capture.screenshot_base64}`}
|
||||
@@ -237,9 +242,47 @@ export default function CapturePanel({
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{library.length > 4 && (
|
||||
<button
|
||||
className="library-show-all"
|
||||
onClick={() => setShowLibraryGallery(true)}
|
||||
>
|
||||
+{library.length - 4} autres
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Galerie plein écran de la bibliothèque */}
|
||||
{showLibraryGallery && (
|
||||
<div className="fullscreen-modal">
|
||||
<div className="fullscreen-header">
|
||||
<span>Bibliothèque — {library.length} captures — Cliquez pour sélectionner</span>
|
||||
<button onClick={() => setShowLibraryGallery(false)}>Fermer (Échap)</button>
|
||||
</div>
|
||||
<div className="library-gallery-grid">
|
||||
{library.map(item => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="library-gallery-item"
|
||||
onClick={() => {
|
||||
setShowLibraryGallery(false);
|
||||
handleLibrarySelect(item);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={`data:image/png;base64,${item.capture.screenshot_base64}`}
|
||||
alt="Capture"
|
||||
/>
|
||||
<div className="library-gallery-label">
|
||||
{new Date(item.timestamp).toLocaleTimeString('fr-FR')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal plein écran */}
|
||||
{isFullscreen && currentCapture && (
|
||||
<FullscreenSelector
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Panneau droit à onglets pour le VWB
|
||||
* 3 onglets : Propriétés, Capture, Données
|
||||
* Panneau redimensionnable : 320px par défaut, 480px quand actif
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import type { Step, Capture, ExecutionMode } from '../types';
|
||||
import PropertiesPanel from './PropertiesPanel';
|
||||
import CapturePanel from './CapturePanel';
|
||||
import CaptureLibrary from './CaptureLibrary';
|
||||
import VariableManager from './VariableManager';
|
||||
import type { Variable } from './VariableManager';
|
||||
|
||||
type TabId = 'properties' | 'capture' | 'data';
|
||||
|
||||
interface Tab {
|
||||
id: TabId;
|
||||
label: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const TABS: Tab[] = [
|
||||
{ id: 'properties', label: 'Propriétés', icon: '⚙' },
|
||||
{ id: 'capture', label: 'Capture', icon: '📷' },
|
||||
{ id: 'data', label: 'Données', icon: '📊' },
|
||||
];
|
||||
|
||||
interface Props {
|
||||
// Propriétés
|
||||
selectedStep: Step | null;
|
||||
onUpdateStepParams: (id: string, params: Record<string, unknown>) => void;
|
||||
onDeleteStep: (id: string) => void;
|
||||
// Capture
|
||||
capture: Capture | null;
|
||||
onCapture: () => void;
|
||||
onSelectAnchor: (bbox: { x: number; y: number; width: number; height: number }, screenshotBase64?: string) => void;
|
||||
hasSelectedStep: boolean;
|
||||
executionMode: ExecutionMode;
|
||||
detectionZone: { x: number; y: number; width: number; height: number } | null;
|
||||
onSetDetectionZone: (zone: { x: number; y: number; width: number; height: number } | null) => void;
|
||||
// Bibliothèque
|
||||
currentCapture: Capture | null;
|
||||
onSelectCaptureFromLibrary: (capture: Capture) => void;
|
||||
// Variables
|
||||
variables: Variable[];
|
||||
onVariableCreate: (data: Omit<Variable, 'id'>) => void;
|
||||
onVariableUpdate: (id: string, data: Partial<Variable>) => void;
|
||||
onVariableDelete: (id: string) => void;
|
||||
steps: Step[];
|
||||
runtimeVariables: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export default function RightPanel({
|
||||
selectedStep,
|
||||
onUpdateStepParams,
|
||||
onDeleteStep,
|
||||
capture,
|
||||
onCapture,
|
||||
onSelectAnchor,
|
||||
hasSelectedStep,
|
||||
executionMode,
|
||||
detectionZone,
|
||||
onSetDetectionZone,
|
||||
currentCapture,
|
||||
onSelectCaptureFromLibrary,
|
||||
variables,
|
||||
onVariableCreate,
|
||||
onVariableUpdate,
|
||||
onVariableDelete,
|
||||
steps,
|
||||
runtimeVariables,
|
||||
}: Props) {
|
||||
const [activeTab, setActiveTab] = useState<TabId>('properties');
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const panelRef = useRef<HTMLElement>(null);
|
||||
|
||||
// Basculer sur l'onglet Propriétés quand une étape est sélectionnée
|
||||
useEffect(() => {
|
||||
if (selectedStep) {
|
||||
setActiveTab('properties');
|
||||
setIsExpanded(true);
|
||||
}
|
||||
}, [selectedStep?.id]);
|
||||
|
||||
// Basculer sur l'onglet Capture quand une nouvelle capture arrive
|
||||
useEffect(() => {
|
||||
if (capture) {
|
||||
setActiveTab('capture');
|
||||
setIsExpanded(true);
|
||||
}
|
||||
}, [capture]);
|
||||
|
||||
// Clic en dehors du panneau pour replier
|
||||
const handleClickOutside = useCallback((e: MouseEvent) => {
|
||||
if (panelRef.current && !panelRef.current.contains(e.target as Node)) {
|
||||
setIsExpanded(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [handleClickOutside]);
|
||||
|
||||
// Clic sur un onglet
|
||||
const handleTabClick = (tabId: TabId) => {
|
||||
if (activeTab === tabId && isExpanded) {
|
||||
// Re-clic sur le même onglet : replier
|
||||
setIsExpanded(false);
|
||||
} else {
|
||||
setActiveTab(tabId);
|
||||
setIsExpanded(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Rendu du contenu de l'onglet actif
|
||||
const renderTabContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'properties':
|
||||
return (
|
||||
<div className="tab-content-inner">
|
||||
<PropertiesPanel
|
||||
step={selectedStep}
|
||||
onUpdateParams={onUpdateStepParams}
|
||||
onDelete={onDeleteStep}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'capture':
|
||||
return (
|
||||
<div className="tab-content-inner">
|
||||
<CapturePanel
|
||||
capture={capture}
|
||||
onCapture={onCapture}
|
||||
onSelectAnchor={onSelectAnchor}
|
||||
hasSelectedStep={hasSelectedStep}
|
||||
executionMode={executionMode}
|
||||
detectionZone={detectionZone}
|
||||
onSetDetectionZone={onSetDetectionZone}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'data':
|
||||
return (
|
||||
<div className="tab-content-inner">
|
||||
<VariableManager
|
||||
variables={variables}
|
||||
onVariableCreate={onVariableCreate}
|
||||
onVariableUpdate={onVariableUpdate}
|
||||
onVariableDelete={onVariableDelete}
|
||||
steps={steps}
|
||||
runtimeVariables={runtimeVariables}
|
||||
/>
|
||||
<CaptureLibrary
|
||||
currentCapture={currentCapture}
|
||||
onSelectCapture={onSelectCaptureFromLibrary}
|
||||
onCapture={onCapture}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<aside
|
||||
ref={panelRef}
|
||||
className={`sidebar right right-panel-tabbed ${isExpanded ? 'expanded' : 'collapsed'}`}
|
||||
onClick={() => !isExpanded && setIsExpanded(true)}
|
||||
>
|
||||
{/* Onglets en haut */}
|
||||
<div className="right-panel-tabs">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`right-panel-tab ${activeTab === tab.id ? 'active' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleTabClick(tab.id);
|
||||
}}
|
||||
title={tab.label}
|
||||
>
|
||||
<span className="tab-icon">{tab.icon}</span>
|
||||
<span className="tab-label">{tab.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Contenu de l'onglet actif */}
|
||||
<div className="right-panel-content">
|
||||
{renderTabContent()}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
import { memo, useState, useEffect, useRef } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import type { Step } from '../types';
|
||||
@@ -34,8 +35,8 @@ function StepNode({ data, selected }: StepNodeProps) {
|
||||
|
||||
return (
|
||||
<div className={`step-node ${selected ? 'selected' : ''} ${isConditional ? 'conditional' : ''} ${isDataLoop ? 'data-loop' : ''} ${isImport ? 'data-import' : ''}`}>
|
||||
{/* Bouton aide (?) — toujours visible quand sélectionné */}
|
||||
{selected && action && (
|
||||
{/* Bouton aide (?) */}
|
||||
{action && (
|
||||
<button
|
||||
className="step-node-help"
|
||||
title="Documentation de l'outil"
|
||||
@@ -75,7 +76,7 @@ function StepNode({ data, selected }: StepNodeProps) {
|
||||
)}
|
||||
|
||||
{/* Bouton supprimer */}
|
||||
{selected && (
|
||||
{(
|
||||
<button
|
||||
className="step-node-delete"
|
||||
title="Supprimer (Suppr)"
|
||||
@@ -94,6 +95,7 @@ function StepNode({ data, selected }: StepNodeProps) {
|
||||
position={Position.Top}
|
||||
id="top"
|
||||
className="handle-top"
|
||||
isConnectable={true}
|
||||
/>
|
||||
|
||||
<div className="step-node-header">
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Service de détection UI (UI-DETR-1)
|
||||
*/
|
||||
|
||||
// Utilise l'hostname actuel pour permettre l'accès réseau
|
||||
const API_BASE = `http://${window.location.hostname}:5001`;
|
||||
// VWB backend (port 5002) — contient le screen capturer et la détection UI
|
||||
const API_BASE = `http://${window.location.hostname}:5002`;
|
||||
|
||||
export interface UIElement {
|
||||
id: number;
|
||||
|
||||
@@ -96,6 +96,21 @@ body {
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Panneau droit à onglets — redimensionnement dynamique */
|
||||
.sidebar.right.right-panel-tabbed {
|
||||
width: 320px;
|
||||
transition: width 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar.right.right-panel-tabbed.expanded {
|
||||
width: 480px;
|
||||
}
|
||||
|
||||
.sidebar.right.right-panel-tabbed.collapsed {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.sidebar h3, .sidebar h4 {
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-sidebar);
|
||||
@@ -4225,3 +4240,148 @@ body {
|
||||
background: var(--bg-sidebar);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Galerie bibliothèque plein écran */
|
||||
.library-gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.library-gallery-item {
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s, transform 0.2s;
|
||||
background: var(--bg-paper);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.library-gallery-item:hover {
|
||||
border-color: var(--primary);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.library-gallery-item img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.library-gallery-label {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
background: var(--bg-default);
|
||||
}
|
||||
|
||||
.library-show-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-default);
|
||||
border: 2px dashed var(--border);
|
||||
border-radius: 6px;
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.library-show-all:hover {
|
||||
background: var(--bg-paper);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Panneau droit à onglets (RightPanel)
|
||||
=========================================== */
|
||||
|
||||
/* Barre d'onglets en haut du panneau droit */
|
||||
.right-panel-tabs {
|
||||
display: flex;
|
||||
background: var(--bg-sidebar);
|
||||
border-bottom: 2px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.right-panel-tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.6rem 0.5rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.right-panel-tab:hover {
|
||||
color: var(--primary);
|
||||
background: rgba(25, 118, 210, 0.04);
|
||||
}
|
||||
|
||||
.right-panel-tab.active {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Bordure inférieure colorée sur l'onglet actif */
|
||||
.right-panel-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.right-panel-tab .tab-icon {
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.right-panel-tab .tab-label {
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
/* Zone de contenu scrollable */
|
||||
.right-panel-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.tab-content-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Ajustements des sous-composants dans le panneau à onglets */
|
||||
.right-panel-tabbed .properties-panel {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.right-panel-tabbed .capture-panel {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.right-panel-tabbed .variable-manager {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.right-panel-tabbed .capture-library {
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user