feat(vwb): Ajouter SeeClick, Self-Healing interactif et Dashboard confiance
## Nouvelles fonctionnalités ### 1. SeeClick Adapter (visual grounding fallback) - Nouvel adapter pour le modèle SeeClick (HuggingFace) - Intégré dans la chaîne de fallback: CLIP → Template → SeeClick → Static - Localise les éléments GUI à partir de descriptions textuelles ### 2. Self-Healing Interactif - Dialogue qui propose des alternatives quand l'ancre n'est pas trouvée - L'utilisateur peut choisir: candidat alternatif, coords statiques, ou sauter - Nouveaux endpoints: /healing/status, /healing/choose, /healing/candidates - État "waiting_for_choice" pour mettre l'exécution en pause ### 3. Dashboard Confiance (temps réel) - Affiche les scores de confiance pendant l'exécution - Montre: méthode utilisée, distance, taux de succès - Interface pliable en bas à droite - Visible uniquement en mode intelligent/debug ## Fichiers ajoutés - core/detection/seeclick_adapter.py - frontend_v4/src/components/SelfHealingDialog.tsx - frontend_v4/src/components/ConfidenceDashboard.tsx ## Fichiers modifiés - core/detection/__init__.py - backend/services/intelligent_executor.py - backend/api_v3/execute.py - frontend_v4/src/App.tsx - frontend_v4/src/services/api.ts - docs/VISION_RPA_INTELLIGENT.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,8 @@ import ExecutionOverlay from './components/ExecutionOverlay';
|
||||
import VariableManager from './components/VariableManager';
|
||||
import type { Variable } from './components/VariableManager';
|
||||
import CaptureLibrary from './components/CaptureLibrary';
|
||||
import SelfHealingDialog from './components/SelfHealingDialog';
|
||||
import ConfidenceDashboard from './components/ConfidenceDashboard';
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
step: StepNode,
|
||||
@@ -44,6 +46,11 @@ function App() {
|
||||
const [showWorkflowManager, setShowWorkflowManager] = useState(false);
|
||||
const [currentCapture, setCurrentCapture] = useState<Capture | null>(null);
|
||||
|
||||
// Self-healing interactif
|
||||
const [showSelfHealing, setShowSelfHealing] = useState(false);
|
||||
const [healingCandidates, setHealingCandidates] = useState<any[]>([]);
|
||||
const [healingStepInfo, setHealingStepInfo] = useState<any>(null);
|
||||
|
||||
// Charger l'état initial
|
||||
const loadState = useCallback(async () => {
|
||||
try {
|
||||
@@ -68,11 +75,19 @@ function App() {
|
||||
const status = await api.getExecutionStatus();
|
||||
setIsExecutionRunning(status.is_running);
|
||||
|
||||
// Self-healing interactif: detecter si on attend un choix utilisateur
|
||||
if (status.waiting_for_choice && status.candidates) {
|
||||
setHealingCandidates(status.candidates);
|
||||
setHealingStepInfo(status.current_step_info);
|
||||
setShowSelfHealing(true);
|
||||
}
|
||||
|
||||
// Mettre à jour l'état si l'exécution est terminée
|
||||
// Note: Ne PAS fermer l'overlay automatiquement pour permettre
|
||||
// à l'utilisateur de voir les résultats de détection
|
||||
if (!status.is_running) {
|
||||
await loadState();
|
||||
setShowSelfHealing(false);
|
||||
// L'overlay reste visible, l'utilisateur peut le fermer manuellement
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -289,6 +304,18 @@ function App() {
|
||||
setVariables(prev => prev.filter(v => v.id !== id));
|
||||
};
|
||||
|
||||
// Self-healing: soumettre le choix de l'utilisateur
|
||||
const handleSelfHealingChoice = async (choice: 'skip' | 'static' | { x: number; y: number }) => {
|
||||
try {
|
||||
await api.submitHealingChoice(choice);
|
||||
setShowSelfHealing(false);
|
||||
setHealingCandidates([]);
|
||||
setHealingStepInfo(null);
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
// Drop d'un outil sur le canvas
|
||||
const onDrop = useCallback(
|
||||
(event: React.DragEvent) => {
|
||||
@@ -433,6 +460,24 @@ function App() {
|
||||
onClose={() => setShowWorkflowManager(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Self-Healing Dialog */}
|
||||
<SelfHealingDialog
|
||||
isOpen={showSelfHealing}
|
||||
candidates={healingCandidates}
|
||||
stepInfo={healingStepInfo}
|
||||
onChoose={handleSelfHealingChoice}
|
||||
onClose={() => {
|
||||
setShowSelfHealing(false);
|
||||
handleStopExecution();
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Confidence Dashboard - scores en temps reel */}
|
||||
<ConfidenceDashboard
|
||||
isExecutionRunning={isExecutionRunning}
|
||||
executionMode={executionMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* Confidence Dashboard Component
|
||||
*
|
||||
* Affiche les scores de confiance en temps réel pendant l'exécution.
|
||||
* Montre CLIP score, template score, distance et méthode utilisée.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface StepScore {
|
||||
stepIndex: number;
|
||||
stepType: string;
|
||||
method: string;
|
||||
confidence: number;
|
||||
distance?: number;
|
||||
clipScore?: number;
|
||||
templateScore?: number;
|
||||
timestamp: number;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
isExecutionRunning: boolean;
|
||||
executionMode: 'basic' | 'intelligent' | 'debug';
|
||||
}
|
||||
|
||||
export default function ConfidenceDashboard({ isExecutionRunning, executionMode }: Props) {
|
||||
const [scores, setScores] = useState<StepScore[]>([]);
|
||||
const [currentStep, setCurrentStep] = useState<number>(0);
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
|
||||
// Polling pour les scores en temps réel
|
||||
useEffect(() => {
|
||||
if (!isExecutionRunning) return;
|
||||
|
||||
const pollScores = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v3/execute/status');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.execution) {
|
||||
setCurrentStep(data.execution.current_step_index || 0);
|
||||
|
||||
// Si on a des resultats d'etapes, les ajouter
|
||||
if (data.execution.step_results) {
|
||||
const newScores: StepScore[] = data.execution.step_results.map((result: any, index: number) => ({
|
||||
stepIndex: index,
|
||||
stepType: result.action_type || 'unknown',
|
||||
method: result.output?.method || 'static',
|
||||
confidence: result.output?.confidence || 1.0,
|
||||
distance: result.output?.distance,
|
||||
clipScore: result.output?.clip_score,
|
||||
templateScore: result.output?.template_score,
|
||||
timestamp: new Date(result.ended_at).getTime(),
|
||||
success: result.status === 'success'
|
||||
}));
|
||||
setScores(newScores);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erreur polling scores:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(pollScores, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [isExecutionRunning]);
|
||||
|
||||
// Reset quand l'execution s'arrete
|
||||
useEffect(() => {
|
||||
if (!isExecutionRunning) {
|
||||
// Garder les scores pour review
|
||||
}
|
||||
}, [isExecutionRunning]);
|
||||
|
||||
if (executionMode === 'basic') {
|
||||
return null; // Pas de dashboard en mode basic
|
||||
}
|
||||
|
||||
const getConfidenceColor = (confidence: number): string => {
|
||||
if (confidence >= 0.8) return '#a6e3a1'; // Vert
|
||||
if (confidence >= 0.5) return '#f9e2af'; // Jaune
|
||||
return '#f38ba8'; // Rouge
|
||||
};
|
||||
|
||||
const getMethodIcon = (method: string): string => {
|
||||
switch (method) {
|
||||
case 'clip': return '🧠';
|
||||
case 'clip_embedding': return '🧠';
|
||||
case 'zoned_template': return '📍';
|
||||
case 'direct_template': return '🔍';
|
||||
case 'seeclick_grounding': return '🎯';
|
||||
case 'static_fallback': return '📌';
|
||||
case 'user_choice': return '👆';
|
||||
default: return '⚡';
|
||||
}
|
||||
};
|
||||
|
||||
const averageConfidence = scores.length > 0
|
||||
? scores.reduce((acc, s) => acc + s.confidence, 0) / scores.length
|
||||
: 0;
|
||||
|
||||
const successRate = scores.length > 0
|
||||
? (scores.filter(s => s.success).length / scores.length) * 100
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="confidence-dashboard">
|
||||
<div className="dashboard-header" onClick={() => setIsExpanded(!isExpanded)}>
|
||||
<div className="header-left">
|
||||
<span className="dashboard-icon">📊</span>
|
||||
<span className="dashboard-title">Scores de confiance</span>
|
||||
{isExecutionRunning && (
|
||||
<span className="live-indicator">LIVE</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="header-right">
|
||||
<span className="toggle-icon">{isExpanded ? '▼' : '▶'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="dashboard-content">
|
||||
{/* Metriques globales */}
|
||||
<div className="metrics-row">
|
||||
<div className="metric">
|
||||
<span className="metric-label">Etape actuelle</span>
|
||||
<span className="metric-value">{currentStep + 1}</span>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<span className="metric-label">Confiance moy.</span>
|
||||
<span
|
||||
className="metric-value"
|
||||
style={{ color: getConfidenceColor(averageConfidence) }}
|
||||
>
|
||||
{(averageConfidence * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<span className="metric-label">Taux succes</span>
|
||||
<span
|
||||
className="metric-value"
|
||||
style={{ color: getConfidenceColor(successRate / 100) }}
|
||||
>
|
||||
{successRate.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Liste des scores par etape */}
|
||||
<div className="scores-list">
|
||||
{scores.length === 0 ? (
|
||||
<div className="no-scores">
|
||||
{isExecutionRunning
|
||||
? "En attente de resultats..."
|
||||
: "Aucune execution en cours"}
|
||||
</div>
|
||||
) : (
|
||||
scores.map((score) => (
|
||||
<div
|
||||
key={score.stepIndex}
|
||||
className={`score-item ${score.success ? 'success' : 'error'} ${score.stepIndex === currentStep ? 'current' : ''}`}
|
||||
>
|
||||
<div className="score-step">
|
||||
<span className="step-number">#{score.stepIndex + 1}</span>
|
||||
<span className="method-icon">{getMethodIcon(score.method)}</span>
|
||||
</div>
|
||||
<div className="score-details">
|
||||
<span className="method-name">{score.method}</span>
|
||||
{score.distance !== undefined && (
|
||||
<span className="distance">{score.distance.toFixed(0)}px</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="confidence-bar"
|
||||
style={{
|
||||
'--confidence': `${score.confidence * 100}%`,
|
||||
'--confidence-color': getConfidenceColor(score.confidence)
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<span className="confidence-value">
|
||||
{(score.confidence * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Legende */}
|
||||
<div className="legend">
|
||||
<span className="legend-item">🧠 CLIP</span>
|
||||
<span className="legend-item">📍 Template zone</span>
|
||||
<span className="legend-item">🎯 SeeClick</span>
|
||||
<span className="legend-item">📌 Static</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style>{`
|
||||
.confidence-dashboard {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 320px;
|
||||
background: #1e1e2e;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid #313244;
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: #313244;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dashboard-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-weight: 600;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
|
||||
.live-indicator {
|
||||
padding: 2px 6px;
|
||||
background: #f38ba8;
|
||||
color: #1e1e2e;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
color: #a6adc8;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.dashboard-content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.metrics-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #313244;
|
||||
}
|
||||
|
||||
.metric {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
color: #a6adc8;
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
|
||||
.scores-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.no-scores {
|
||||
text-align: center;
|
||||
color: #a6adc8;
|
||||
padding: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.score-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
margin-bottom: 4px;
|
||||
background: #313244;
|
||||
border-radius: 6px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.score-item.current {
|
||||
border: 1px solid #89b4fa;
|
||||
background: rgba(137, 180, 250, 0.1);
|
||||
}
|
||||
|
||||
.score-item.error {
|
||||
border-left: 3px solid #f38ba8;
|
||||
}
|
||||
|
||||
.score-item.success {
|
||||
border-left: 3px solid #a6e3a1;
|
||||
}
|
||||
|
||||
.score-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
color: #89b4fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.method-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.score-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.method-name {
|
||||
color: #cdd6f4;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.distance {
|
||||
color: #fab387;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.confidence-bar {
|
||||
width: 60px;
|
||||
height: 20px;
|
||||
background: #45475a;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.confidence-bar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: var(--confidence);
|
||||
background: var(--confidence-color);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.confidence-value {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
color: #1e1e2e;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #313244;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
font-size: 10px;
|
||||
color: #a6adc8;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
/**
|
||||
* Self-Healing Dialog Component
|
||||
*
|
||||
* Affiche les candidats alternatifs quand l'ancre n'est pas trouvée
|
||||
* et permet à l'utilisateur de choisir une action.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface Candidate {
|
||||
id: number;
|
||||
element_id: number;
|
||||
score: number;
|
||||
bbox: {
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
};
|
||||
center?: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
distance?: number;
|
||||
method?: string;
|
||||
}
|
||||
|
||||
interface StepInfo {
|
||||
index: number;
|
||||
total: number;
|
||||
original_bbox?: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
candidates: Candidate[];
|
||||
stepInfo: StepInfo | null;
|
||||
onChoose: (choice: 'skip' | 'static' | { x: number; y: number }) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function SelfHealingDialog({ isOpen, candidates, stepInfo, onChoose, onClose }: Props) {
|
||||
const [selectedCandidate, setSelectedCandidate] = useState<number | null>(null);
|
||||
const [customCoords, setCustomCoords] = useState({ x: '', y: '' });
|
||||
|
||||
// Reset quand le dialog s'ouvre
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setSelectedCandidate(null);
|
||||
setCustomCoords({ x: '', y: '' });
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleCandidateClick = (candidate: Candidate) => {
|
||||
setSelectedCandidate(candidate.id);
|
||||
if (candidate.center) {
|
||||
setCustomCoords({
|
||||
x: candidate.center.x.toString(),
|
||||
y: candidate.center.y.toString()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (customCoords.x && customCoords.y) {
|
||||
onChoose({
|
||||
x: parseInt(customCoords.x),
|
||||
y: parseInt(customCoords.y)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="self-healing-overlay">
|
||||
<div className="self-healing-dialog">
|
||||
<div className="dialog-header">
|
||||
<h2>Self-Healing Required</h2>
|
||||
<button className="close-btn" onClick={onClose}>×</button>
|
||||
</div>
|
||||
|
||||
<div className="dialog-content">
|
||||
{stepInfo && (
|
||||
<div className="step-info">
|
||||
<span className="step-badge">
|
||||
Etape {stepInfo.index + 1}/{stepInfo.total}
|
||||
</span>
|
||||
<p className="error-message">{stepInfo.error || "L'ancre visuelle n'a pas ete trouvee"}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="options-section">
|
||||
<h3>Alternatives possibles</h3>
|
||||
|
||||
{candidates.length > 0 ? (
|
||||
<div className="candidates-list">
|
||||
{candidates.map((candidate) => (
|
||||
<div
|
||||
key={candidate.id}
|
||||
className={`candidate-item ${selectedCandidate === candidate.id ? 'selected' : ''}`}
|
||||
onClick={() => handleCandidateClick(candidate)}
|
||||
>
|
||||
<div className="candidate-info">
|
||||
<span className="candidate-id">#{candidate.element_id}</span>
|
||||
<span className="candidate-score">
|
||||
Confiance: {(candidate.score * 100).toFixed(0)}%
|
||||
</span>
|
||||
{candidate.distance !== undefined && (
|
||||
<span className="candidate-distance">
|
||||
Distance: {candidate.distance.toFixed(0)}px
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{candidate.center && (
|
||||
<div className="candidate-coords">
|
||||
({candidate.center.x}, {candidate.center.y})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="no-candidates">Aucun candidat similaire trouve</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="manual-section">
|
||||
<h3>Coordonnees manuelles</h3>
|
||||
<div className="coords-input">
|
||||
<label>
|
||||
X:
|
||||
<input
|
||||
type="number"
|
||||
value={customCoords.x}
|
||||
onChange={(e) => setCustomCoords(prev => ({ ...prev, x: e.target.value }))}
|
||||
placeholder="X"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Y:
|
||||
<input
|
||||
type="number"
|
||||
value={customCoords.y}
|
||||
onChange={(e) => setCustomCoords(prev => ({ ...prev, y: e.target.value }))}
|
||||
placeholder="Y"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dialog-actions">
|
||||
<button
|
||||
className="action-btn primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={!customCoords.x || !customCoords.y}
|
||||
>
|
||||
Utiliser ces coordonnees
|
||||
</button>
|
||||
<button
|
||||
className="action-btn secondary"
|
||||
onClick={() => onChoose('static')}
|
||||
>
|
||||
Utiliser position originale
|
||||
</button>
|
||||
<button
|
||||
className="action-btn warning"
|
||||
onClick={() => onChoose('skip')}
|
||||
>
|
||||
Sauter cette etape
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.self-healing-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.self-healing-dialog {
|
||||
background: #1e1e2e;
|
||||
border-radius: 12px;
|
||||
width: 500px;
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid #313244;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
background: #313244;
|
||||
border-bottom: 1px solid #45475a;
|
||||
}
|
||||
|
||||
.dialog-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #f5c2e7;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #a6adc8;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #f38ba8;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-info {
|
||||
margin-bottom: 20px;
|
||||
padding: 12px;
|
||||
background: #313244;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.step-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
background: #89b4fa;
|
||||
color: #1e1e2e;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin: 0;
|
||||
color: #f9e2af;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.options-section, .manual-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #cdd6f4;
|
||||
font-size: 14px;
|
||||
margin: 0 0 12px 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.candidates-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.candidate-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: #313244;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.candidate-item:hover {
|
||||
background: #45475a;
|
||||
}
|
||||
|
||||
.candidate-item.selected {
|
||||
border-color: #89b4fa;
|
||||
background: rgba(137, 180, 250, 0.1);
|
||||
}
|
||||
|
||||
.candidate-info {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.candidate-id {
|
||||
color: #89b4fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.candidate-score {
|
||||
color: #a6e3a1;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.candidate-distance {
|
||||
color: #fab387;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.candidate-coords {
|
||||
color: #a6adc8;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.no-candidates {
|
||||
color: #a6adc8;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.coords-input {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.coords-input label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #a6adc8;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.coords-input input {
|
||||
width: 100px;
|
||||
padding: 8px 12px;
|
||||
background: #313244;
|
||||
border: 1px solid #45475a;
|
||||
border-radius: 6px;
|
||||
color: #cdd6f4;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.coords-input input:focus {
|
||||
outline: none;
|
||||
border-color: #89b4fa;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
background: #313244;
|
||||
border-top: 1px solid #45475a;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: #89b4fa;
|
||||
color: #1e1e2e;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover:not(:disabled) {
|
||||
background: #b4befe;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: #45475a;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
|
||||
.action-btn.secondary:hover {
|
||||
background: #585b70;
|
||||
}
|
||||
|
||||
.action-btn.warning {
|
||||
background: #f38ba8;
|
||||
color: #1e1e2e;
|
||||
}
|
||||
|
||||
.action-btn.warning:hover {
|
||||
background: #eba0ac;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* API Client - Toutes les interactions avec le backend
|
||||
*/
|
||||
|
||||
import type { AppState, Workflow, Step, Execution, Capture, ActionType } from '../types';
|
||||
import type { AppState, Workflow, Step, Execution, Capture, ActionType, ExecutionMode } from '../types';
|
||||
|
||||
const API_BASE = '/api/v3';
|
||||
|
||||
@@ -61,6 +61,18 @@ export async function deleteWorkflow(workflowId: string): Promise<{ deleted_id:
|
||||
return request('DELETE', `/workflow/${workflowId}`);
|
||||
}
|
||||
|
||||
export async function updateWorkflow(
|
||||
workflowId: string,
|
||||
updates: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
triggerExamples?: string[];
|
||||
}
|
||||
): Promise<{ workflow: Workflow }> {
|
||||
return request('PUT', `/workflow/${workflowId}`, updates);
|
||||
}
|
||||
|
||||
// Steps
|
||||
export async function addStep(
|
||||
workflowId: string,
|
||||
@@ -126,8 +138,14 @@ export function getAnchorThumbnailUrl(anchorId: string): string {
|
||||
}
|
||||
|
||||
// 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 startExecution(
|
||||
workflowId?: string,
|
||||
mode?: ExecutionMode
|
||||
): Promise<{ execution: Execution; session: AppState['session'] }> {
|
||||
return request('POST', '/execute/start', {
|
||||
workflow_id: workflowId,
|
||||
execution_mode: mode || 'basic'
|
||||
});
|
||||
}
|
||||
|
||||
export async function pauseExecution(): Promise<{ execution: Execution }> {
|
||||
@@ -147,6 +165,29 @@ export async function getExecutionStatus(): Promise<{
|
||||
is_paused: boolean;
|
||||
execution: Execution | null;
|
||||
session: AppState['session'];
|
||||
// Self-healing interactif
|
||||
waiting_for_choice?: boolean;
|
||||
candidates?: Array<{
|
||||
id: number;
|
||||
element_id: number;
|
||||
score: number;
|
||||
bbox: { x1: number; y1: number; x2: number; y2: number };
|
||||
center?: { x: number; y: number };
|
||||
distance?: number;
|
||||
}>;
|
||||
current_step_info?: {
|
||||
index: number;
|
||||
total: number;
|
||||
original_bbox?: { x: number; y: number; width: number; height: number };
|
||||
error?: string;
|
||||
};
|
||||
}> {
|
||||
return request('GET', '/execute/status');
|
||||
}
|
||||
|
||||
// Self-Healing Interactif
|
||||
export async function submitHealingChoice(
|
||||
choice: 'skip' | 'static' | { x: number; y: number }
|
||||
): Promise<{ success: boolean; choice: unknown }> {
|
||||
return request('POST', '/execute/healing/choose', { choice });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user