refactor(vwb): refonte complète capture écran — stable définitivement
Some checks failed
security-audit / Bandit (scan statique) (push) Successful in 13s
security-audit / pip-audit (CVE dépendances) (push) Successful in 11s
security-audit / Scan secrets (grep) (push) Successful in 8s
tests / Lint (ruff + black) (push) Successful in 13s
tests / Tests unitaires (sans GPU) (push) Failing after 15s
tests / Tests sécurité (critique) (push) Has been skipped

FullscreenSelector réécrit :
- Overlay unique positionné via getBoundingClientRect()
- Recalcul auto au resize
- Coordonnées souris relatives à l'image
- Plus de décalage bboxes/sélection

Capture backend :
- mss.monitors[0] (écran composite) au lieu de pyautogui.screenshot()
- Capture la VM en plein écran correctement

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-21 09:03:19 +02:00
parent 5da4581e76
commit 14a9442343
2 changed files with 185 additions and 99 deletions

View File

@@ -52,10 +52,24 @@ def capture_screen():
} }
""" """
try: try:
import pyautogui # Utiliser mss pour capturer TOUS les moniteurs (ecran compose).
# pyautogui.screenshot() capture uniquement le premier moniteur,
# ce qui rate la VM en plein ecran sur un second ecran ou via QEMU/spice.
# mss.monitors[0] = ecran compose (tous les moniteurs), ce qui capture
# exactement ce que l'utilisateur voit quel que soit le setup.
try:
import mss
with mss.mss() as sct:
# monitors[0] = ecran virtuel englobant tous les moniteurs
monitor = sct.monitors[0]
sct_img = sct.grab(monitor)
# Convertir mss ScreenShot (BGRA) en PIL Image RGB
screenshot = Image.frombytes('RGB', sct_img.size, sct_img.rgb)
except ImportError:
# Fallback pyautogui si mss n'est pas installe
import pyautogui
screenshot = pyautogui.screenshot()
# Capture écran
screenshot = pyautogui.screenshot()
width, height = screenshot.size width, height = screenshot.size
# Convertir en base64 # Convertir en base64

View File

@@ -1,4 +1,4 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect, useCallback } from 'react';
import type { Capture, ExecutionMode } from '../types'; import type { Capture, ExecutionMode } from '../types';
import type { UIElement } from '../services/uiDetection'; import type { UIElement } from '../services/uiDetection';
import { import {
@@ -7,6 +7,10 @@ import {
compressThumbnail, compressThumbnail,
} from '../services/captureLibraryStorage'; } from '../services/captureLibraryStorage';
/**
* Auto-detecte PNG vs JPEG depuis le contenu base64 et retourne un data: URL.
* GARDER CETTE FONCTION : elle evite les bugs d'affichage PNG/JPEG.
*/
function b64ImgSrc(base64: string): string { function b64ImgSrc(base64: string): string {
if (base64.startsWith('data:')) return base64; if (base64.startsWith('data:')) return base64;
const mime = base64.startsWith('/9j/') ? 'image/jpeg' : 'image/png'; const mime = base64.startsWith('/9j/') ? 'image/jpeg' : 'image/png';
@@ -53,7 +57,7 @@ export default function CapturePanel({
const [currentCapture, setCurrentCapture] = useState<Capture | null>(null); const [currentCapture, setCurrentCapture] = useState<Capture | null>(null);
const [timerSeconds, setTimerSeconds] = useState(0); const [timerSeconds, setTimerSeconds] = useState(0);
const [countdown, setCountdown] = useState<number | null>(null); const [countdown, setCountdown] = useState<number | null>(null);
// Éléments détectés sur l'aperçu miniature // Elements detectes sur l'apercu miniature
const [previewElements, setPreviewElements] = useState<UIElement[]>([]); const [previewElements, setPreviewElements] = useState<UIElement[]>([]);
const [isDetectingPreview, setIsDetectingPreview] = useState(false); const [isDetectingPreview, setIsDetectingPreview] = useState(false);
const previewImgRef = useRef<HTMLImageElement>(null); const previewImgRef = useRef<HTMLImageElement>(null);
@@ -61,8 +65,7 @@ export default function CapturePanel({
const isDebugMode = executionMode === 'debug'; const isDebugMode = executionMode === 'debug';
// Charger la bibliothèque depuis le backend (prioritaire), fallback localStorage. // Charger la bibliotheque depuis le backend (prioritaire), fallback localStorage.
// Le helper loadLibraryAsync() migre aussi les données de l'ancienne clé 'captureLibrary'.
useEffect(() => { useEffect(() => {
loadLibraryAsync().then((loaded) => { loadLibraryAsync().then((loaded) => {
setLibrary( setLibrary(
@@ -77,12 +80,12 @@ export default function CapturePanel({
}); });
}, []); }, []);
// Sauvegarder la bibliothèque (localStorage + gestion de quota) // Sauvegarder la bibliotheque (localStorage + backend)
useEffect(() => { useEffect(() => {
saveLibrary(library); saveLibrary(library);
}, [library]); }, [library]);
// Ajouter capture à la bibliothèque (thumbnail compressé JPEG 320x240) // Ajouter capture a la bibliotheque (thumbnail compresse JPEG 320x240)
useEffect(() => { useEffect(() => {
if (!capture) return; if (!capture) return;
setCurrentCapture(capture); setCurrentCapture(capture);
@@ -101,7 +104,7 @@ export default function CapturePanel({
return () => { cancelled = true; }; return () => { cancelled = true; };
}, [capture]); }, [capture]);
// Détecter les éléments UI quand une capture arrive // Detecter les elements UI quand une capture arrive
useEffect(() => { useEffect(() => {
if (!currentCapture) { if (!currentCapture) {
setPreviewElements([]); setPreviewElements([]);
@@ -118,7 +121,7 @@ export default function CapturePanel({
); );
setPreviewElements(result.elements); setPreviewElements(result.elements);
} catch (err) { } catch (err) {
console.error('Détection aperçu échouée:', err); console.error('Detection apercu echouee:', err);
setPreviewElements([]); setPreviewElements([]);
} finally { } finally {
setIsDetectingPreview(false); setIsDetectingPreview(false);
@@ -128,7 +131,7 @@ export default function CapturePanel({
runDetection(); runDetection();
}, [currentCapture]); }, [currentCapture]);
// Calculer le scale de l'aperçu quand l'image est chargée // Calculer le scale de l'apercu quand l'image est chargee
const handlePreviewImageLoad = () => { const handlePreviewImageLoad = () => {
if (previewImgRef.current) { if (previewImgRef.current) {
setPreviewScale({ setPreviewScale({
@@ -153,7 +156,6 @@ export default function CapturePanel({
} as any); } as any);
return; return;
} }
// Agent indisponible — fallback capture locale
console.warn('Agent Windows indisponible, fallback local:', data.error); console.warn('Agent Windows indisponible, fallback local:', data.error);
} catch (err) { } catch (err) {
console.warn('Erreur capture Windows, fallback local:', err); console.warn('Erreur capture Windows, fallback local:', err);
@@ -185,7 +187,7 @@ export default function CapturePanel({
const handleLibrarySelect = (item: LibraryItem) => { const handleLibrarySelect = (item: LibraryItem) => {
setCurrentCapture(item.capture); setCurrentCapture(item.capture);
setIsFullscreen(true); // Ouvrir en plein écran pour sélectionner setIsFullscreen(true);
}; };
const handleDeleteLibraryItem = (id: string) => { const handleDeleteLibraryItem = (id: string) => {
@@ -196,13 +198,13 @@ export default function CapturePanel({
<div className="capture-panel"> <div className="capture-panel">
<h3>Capture</h3> <h3>Capture</h3>
{/* Capture — auto-détection OS navigateur */} {/* Capture — auto-detection OS navigateur */}
<div className="capture-controls"> <div className="capture-controls">
<button disabled={countdown !== null} onClick={doSmartCapture}> <button disabled={countdown !== null} onClick={doSmartCapture}>
📸 Capturer Capturer
</button> </button>
<select value={timerSeconds} onChange={(e) => setTimerSeconds(Number(e.target.value))}> <select value={timerSeconds} onChange={(e) => setTimerSeconds(Number(e.target.value))}>
<option value="0">Immédiat</option> <option value="0">Immediat</option>
<option value="3">3 sec</option> <option value="3">3 sec</option>
<option value="5">5 sec</option> <option value="5">5 sec</option>
<option value="10">10 sec</option> <option value="10">10 sec</option>
@@ -212,7 +214,7 @@ export default function CapturePanel({
</button> </button>
</div> </div>
{/* Aperçu de la capture avec détection */} {/* Apercu de la capture avec detection */}
{currentCapture && ( {currentCapture && (
<div className="capture-preview"> <div className="capture-preview">
<div className="capture-preview-container"> <div className="capture-preview-container">
@@ -223,7 +225,7 @@ export default function CapturePanel({
onClick={() => setIsFullscreen(true)} onClick={() => setIsFullscreen(true)}
onLoad={handlePreviewImageLoad} onLoad={handlePreviewImageLoad}
/> />
{/* Bounding boxes des éléments détectés sur l'aperçu miniature */} {/* Bounding boxes des elements detectes sur l'apercu miniature */}
{previewElements.map((elem) => ( {previewElements.map((elem) => (
<div <div
key={elem.id} key={elem.id}
@@ -239,43 +241,43 @@ export default function CapturePanel({
/> />
))} ))}
{isDetectingPreview && ( {isDetectingPreview && (
<div className="capture-preview-detecting">Détection...</div> <div className="capture-preview-detecting">Detection...</div>
)} )}
</div> </div>
<p className="capture-info"> <p className="capture-info">
{currentCapture.width}x{currentCapture.height} {currentCapture.width}x{currentCapture.height}
{previewElements.length > 0 && ( {previewElements.length > 0 && (
<span className="capture-elements-count"> <span className="capture-elements-count">
{previewElements.length} éments {previewElements.length} elements
</span> </span>
)} )}
<button onClick={() => setIsFullscreen(true)}>Plein écran</button> <button onClick={() => setIsFullscreen(true)}>Plein ecran</button>
</p> </p>
</div> </div>
)} )}
{!hasSelectedStep && currentCapture && ( {!hasSelectedStep && currentCapture && (
<p className="capture-hint">Sélectionnez une étape pour définir l'ancre</p> <p className="capture-hint">Selectionnez une etape pour definir l'ancre</p>
)} )}
{/* Zone de détection */} {/* Zone de detection */}
{detectionZone && ( {detectionZone && (
<div className="detection-zone-info"> <div className="detection-zone-info">
<span>Zone de détection: {detectionZone.width}x{detectionZone.height}</span> <span>Zone de detection: {detectionZone.width}x{detectionZone.height}</span>
<button onClick={() => onSetDetectionZone?.(null)}>Effacer</button> <button onClick={() => onSetDetectionZone?.(null)}>Effacer</button>
</div> </div>
)} )}
{!detectionZone && currentCapture && (executionMode === 'intelligent' || executionMode === 'debug') && ( {!detectionZone && currentCapture && (executionMode === 'intelligent' || executionMode === 'debug') && (
<p className="capture-hint zone-hint"> <p className="capture-hint zone-hint">
Cliquez sur "Plein écran" puis "Zone de détection" pour cibler une zone Cliquez sur "Plein ecran" puis "Zone de detection" pour cibler une zone
</p> </p>
)} )}
{/* Bibliothèque */} {/* Bibliotheque */}
<div className="capture-library"> <div className="capture-library">
<h4 style={{ cursor: 'pointer' }} onClick={() => library.length > 0 && setShowLibraryGallery(true)}> <h4 style={{ cursor: 'pointer' }} onClick={() => library.length > 0 && setShowLibraryGallery(true)}>
Bibliothèque ({library.length}) Bibliotheque ({library.length})
{library.length > 0 && <span style={{ fontSize: '11px', marginLeft: '8px', color: 'var(--primary)' }}>Ouvrir</span>} {library.length > 0 && <span style={{ fontSize: '11px', marginLeft: '8px', color: 'var(--primary)' }}>Ouvrir</span>}
</h4> </h4>
<div className="library-grid"> <div className="library-grid">
{library.slice(0, 4).map(item => ( {library.slice(0, 4).map(item => (
@@ -289,7 +291,7 @@ export default function CapturePanel({
className="delete-btn" className="delete-btn"
onClick={(e) => { e.stopPropagation(); handleDeleteLibraryItem(item.id); }} onClick={(e) => { e.stopPropagation(); handleDeleteLibraryItem(item.id); }}
> >
× x
</button> </button>
</div> </div>
))} ))}
@@ -304,12 +306,12 @@ export default function CapturePanel({
</div> </div>
</div> </div>
{/* Galerie plein écran de la bibliothèque */} {/* Galerie plein ecran de la bibliotheque */}
{showLibraryGallery && ( {showLibraryGallery && (
<div className="fullscreen-modal"> <div className="fullscreen-modal">
<div className="fullscreen-header"> <div className="fullscreen-header">
<span>Bibliothèque {library.length} captures Cliquez pour sélectionner</span> <span>Bibliotheque -- {library.length} captures -- Cliquez pour selectionner</span>
<button onClick={() => setShowLibraryGallery(false)}>Fermer (Échap)</button> <button onClick={() => setShowLibraryGallery(false)}>Fermer (Echap)</button>
</div> </div>
<div className="library-gallery-grid"> <div className="library-gallery-grid">
{library.map(item => ( {library.map(item => (
@@ -334,7 +336,7 @@ export default function CapturePanel({
</div> </div>
)} )}
{/* Modal plein écran */} {/* Modal plein ecran */}
{isFullscreen && currentCapture && ( {isFullscreen && currentCapture && (
<FullscreenSelector <FullscreenSelector
capture={currentCapture} capture={currentCapture}
@@ -353,7 +355,24 @@ export default function CapturePanel({
); );
} }
// Composant sélecteur plein écran // ============================================================================
// FullscreenSelector — composant refonde pour stabilite definitive
//
// Architecture :
// .fullscreen-modal (fixed, flex column)
// .fullscreen-header (bar d'outils)
// .fullscreen-content (flex: 1, flex center, overflow hidden, position relative)
// <img> (max-width/max-height 100%, object-fit contain)
// <div overlay> (position absolute, memes dimensions que l'image via imageRect)
// bboxes detection (position absolute, coords = naturel * scale)
// zone detection (position absolute, coords = naturel * scale)
// selection utilisateur (position absolute, coords relatives a l'image affichee)
//
// PRINCIPE CLE : l'overlay est positionne exactement sur l'image via imageRect.
// Tous les overlays sont enfants de cet overlay, positionnes relativement a lui.
// Pas de calcul d'offset disperse — tout passe par imageRect.
// ============================================================================
function FullscreenSelector({ function FullscreenSelector({
capture, capture,
onClose, onClose,
@@ -372,17 +391,24 @@ function FullscreenSelector({
onSetDetectionZone?: (zone: DetectionZone | null) => void; onSetDetectionZone?: (zone: DetectionZone | null) => void;
}) { }) {
const imgRef = useRef<HTMLImageElement>(null); const imgRef = useRef<HTMLImageElement>(null);
const overlayRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const [isSelecting, setIsSelecting] = useState(false); const [isSelecting, setIsSelecting] = useState(false);
const [startPos, setStartPos] = useState({ x: 0, y: 0 }); const [startPos, setStartPos] = useState({ x: 0, y: 0 });
const [selection, setSelection] = useState({ x: 0, y: 0, width: 0, height: 0 }); const [selection, setSelection] = useState({ x: 0, y: 0, width: 0, height: 0 });
const [detectedElements, setDetectedElements] = useState<UIElement[]>([]); const [detectedElements, setDetectedElements] = useState<UIElement[]>([]);
const [isDetecting, setIsDetecting] = useState(false); const [isDetecting, setIsDetecting] = useState(false);
const [imageScale, setImageScale] = useState({ x: 1, y: 1 });
const [selectionMode, setSelectionMode] = useState<'anchor' | 'zone'>('anchor'); const [selectionMode, setSelectionMode] = useState<'anchor' | 'zone'>('anchor');
const [showDetection, setShowDetection] = useState(true); const [showDetection, setShowDetection] = useState(true);
// Lancer la détection automatiquement (tous modes) // imageRect: position et dimensions de l'image AFFICHEE dans le conteneur
// Calcule par getBoundingClientRect() de l'image relativement au conteneur parent
const [imageRect, setImageRect] = useState({ left: 0, top: 0, width: 0, height: 0 });
// Scale de l'image affichee par rapport a l'image naturelle
const [imageScale, setImageScale] = useState({ x: 1, y: 1 });
// ── Detection automatique des elements UI ──
useEffect(() => { useEffect(() => {
const runDetection = async () => { const runDetection = async () => {
setIsDetecting(true); setIsDetecting(true);
@@ -394,7 +420,7 @@ function FullscreenSelector({
); );
setDetectedElements(result.elements); setDetectedElements(result.elements);
} catch (err) { } catch (err) {
console.error('Erreur tection:', err); console.error('Erreur detection:', err);
} finally { } finally {
setIsDetecting(false); setIsDetecting(false);
} }
@@ -403,17 +429,38 @@ function FullscreenSelector({
runDetection(); runDetection();
}, [capture.screenshot_base64]); }, [capture.screenshot_base64]);
// Calculer le scale quand l'image est chargée // ── Calcul de imageRect et imageScale ──
// Appele au chargement de l'image ET au redimensionnement de la fenetre.
const recalcImageRect = useCallback(() => {
if (!imgRef.current || !contentRef.current) return;
const containerRect = contentRef.current.getBoundingClientRect();
const imgBounds = imgRef.current.getBoundingClientRect();
setImageRect({
left: imgBounds.left - containerRect.left,
top: imgBounds.top - containerRect.top,
width: imgBounds.width,
height: imgBounds.height,
});
setImageScale({
x: imgBounds.width / imgRef.current.naturalWidth,
y: imgBounds.height / imgRef.current.naturalHeight,
});
}, []);
const handleImageLoad = () => { const handleImageLoad = () => {
if (imgRef.current) { recalcImageRect();
setImageScale({
x: imgRef.current.width / imgRef.current.naturalWidth,
y: imgRef.current.height / imgRef.current.naturalHeight
});
}
}; };
// Cliquer sur un élément détecté // Recalculer sur resize
useEffect(() => {
window.addEventListener('resize', recalcImageRect);
return () => window.removeEventListener('resize', recalcImageRect);
}, [recalcImageRect]);
// ── Clic sur un element detecte ──
const handleElementClick = (elem: UIElement) => { const handleElementClick = (elem: UIElement) => {
if (!enabled) return; if (!enabled) return;
const bbox = { const bbox = {
@@ -425,6 +472,7 @@ function FullscreenSelector({
onSelect(bbox); onSelect(bbox);
}; };
// ── Fermeture par Echap ──
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose(); if (e.key === 'Escape') onClose();
@@ -433,35 +481,48 @@ function FullscreenSelector({
return () => document.removeEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown);
}, [onClose]); }, [onClose]);
const handleMouseDown = (e: React.MouseEvent) => { // ── Gestion de la selection par dessin (souris) ──
// Permettre le dessin en mode zone même sans étape sélectionnée // Toutes les coordonnees sont relatives a l'image AFFICHEE (pas au conteneur).
const canDraw = selectionMode === 'zone' || enabled; // On les convertit en coordonnees image naturelle au mouseUp.
if (!canDraw || !imgRef.current) return;
const rect = imgRef.current.getBoundingClientRect(); const getImageRelativePos = (e: React.MouseEvent): { x: number; y: number } | null => {
const x = e.clientX - rect.left; if (!imgRef.current) return null;
const y = e.clientY - rect.top; const imgBounds = imgRef.current.getBoundingClientRect();
const x = e.clientX - imgBounds.left;
const y = e.clientY - imgBounds.top;
// Limiter aux bornes de l'image
return {
x: Math.max(0, Math.min(x, imgBounds.width)),
y: Math.max(0, Math.min(y, imgBounds.height)),
};
};
const handleMouseDown = (e: React.MouseEvent) => {
const canDraw = selectionMode === 'zone' || enabled;
if (!canDraw) return;
const pos = getImageRelativePos(e);
if (!pos) return;
setIsSelecting(true); setIsSelecting(true);
setStartPos({ x, y }); setStartPos(pos);
setSelection({ x, y, width: 0, height: 0 }); setSelection({ x: pos.x, y: pos.y, width: 0, height: 0 });
}; };
const handleMouseMove = (e: React.MouseEvent) => { const handleMouseMove = (e: React.MouseEvent) => {
if (!isSelecting || !imgRef.current) return; if (!isSelecting) return;
const rect = imgRef.current.getBoundingClientRect(); const pos = getImageRelativePos(e);
const currentX = e.clientX - rect.left; if (!pos) return;
const currentY = e.clientY - rect.top;
const width = currentX - startPos.x; const width = pos.x - startPos.x;
const height = currentY - startPos.y; const height = pos.y - startPos.y;
setSelection({ setSelection({
x: width < 0 ? currentX : startPos.x, x: width < 0 ? pos.x : startPos.x,
y: height < 0 ? currentY : startPos.y, y: height < 0 ? pos.y : startPos.y,
width: Math.abs(width), width: Math.abs(width),
height: Math.abs(height) height: Math.abs(height),
}); });
}; };
@@ -474,24 +535,22 @@ function FullscreenSelector({
return; return;
} }
// Convertir en coordonnées réelles de l'image // Convertir en coordonnees naturelles de l'image
const scaleX = imgRef.current.naturalWidth / imgRef.current.width; const scaleX = imgRef.current.naturalWidth / imgRef.current.getBoundingClientRect().width;
const scaleY = imgRef.current.naturalHeight / imgRef.current.height; const scaleY = imgRef.current.naturalHeight / imgRef.current.getBoundingClientRect().height;
const realBbox = { const realBbox = {
x: Math.round(selection.x * scaleX), x: Math.round(selection.x * scaleX),
y: Math.round(selection.y * scaleY), y: Math.round(selection.y * scaleY),
width: Math.round(selection.width * scaleX), width: Math.round(selection.width * scaleX),
height: Math.round(selection.height * scaleY) height: Math.round(selection.height * scaleY),
}; };
if (selectionMode === 'zone' && onSetDetectionZone) { if (selectionMode === 'zone' && onSetDetectionZone) {
// Mode zone de détection - fonctionne même sans étape sélectionnée
onSetDetectionZone(realBbox); onSetDetectionZone(realBbox);
setSelectionMode('anchor'); // Revenir au mode ancre setSelectionMode('anchor');
setSelection({ x: 0, y: 0, width: 0, height: 0 }); setSelection({ x: 0, y: 0, width: 0, height: 0 });
} else if (enabled) { } else if (enabled) {
// Mode ancre normal - nécessite une étape sélectionnée
onSelect(realBbox); onSelect(realBbox);
} }
}; };
@@ -501,21 +560,20 @@ function FullscreenSelector({
<div className="fullscreen-header"> <div className="fullscreen-header">
<div className="header-left"> <div className="header-left">
<span> <span>
{isDetecting && 'Détection en cours... '} {isDetecting && 'Detection en cours... '}
{!isDetecting && detectedElements.length > 0 && `${detectedElements.length} éléments - `} {!isDetecting && detectedElements.length > 0 && `${detectedElements.length} elements - `}
{selectionMode === 'zone' {selectionMode === 'zone'
? 'Dessinez la zone de détection' ? 'Dessinez la zone de detection'
: (enabled ? 'Dessinez un rectangle pour l\'ancre ou cliquez sur un élément détecté' : 'Sélectionnez d\'abord une étape')} : (enabled ? 'Dessinez un rectangle pour l\'ancre ou cliquez sur un element detecte' : 'Selectionnez d\'abord une etape')}
</span> </span>
</div> </div>
<div className="header-center"> <div className="header-center">
{/* Bouton afficher/masquer la détection */}
<button <button
className={`zone-select-btn ${showDetection ? 'active' : ''}`} className={`zone-select-btn ${showDetection ? 'active' : ''}`}
onClick={() => setShowDetection(!showDetection)} onClick={() => setShowDetection(!showDetection)}
style={showDetection ? { background: 'var(--secondary)', borderColor: 'var(--secondary)', color: 'white' } : {}} style={showDetection ? { background: 'var(--secondary)', borderColor: 'var(--secondary)', color: 'white' } : {}}
> >
{showDetection ? 'Masquer détection' : 'Afficher détection'} {showDetection ? 'Masquer detection' : 'Afficher detection'}
</button> </button>
{onSetDetectionZone && ( {onSetDetectionZone && (
<> <>
@@ -523,7 +581,7 @@ function FullscreenSelector({
className={`zone-select-btn ${selectionMode === 'zone' ? 'active' : ''}`} className={`zone-select-btn ${selectionMode === 'zone' ? 'active' : ''}`}
onClick={() => setSelectionMode(selectionMode === 'zone' ? 'anchor' : 'zone')} onClick={() => setSelectionMode(selectionMode === 'zone' ? 'anchor' : 'zone')}
> >
{selectionMode === 'zone' ? 'Annuler' : 'Zone de détection'} {selectionMode === 'zone' ? 'Annuler' : 'Zone de detection'}
</button> </button>
{detectionZone && ( {detectionZone && (
<button <button
@@ -536,26 +594,40 @@ function FullscreenSelector({
</> </>
)} )}
</div> </div>
<button onClick={onClose}>Fermer (Échap)</button> <button onClick={onClose}>Fermer (Echap)</button>
</div> </div>
{/* Zone de contenu : flex center, l'image se dimensionne via max-width/max-height */}
<div <div
className="fullscreen-content" className="fullscreen-content"
ref={contentRef}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp} onMouseUp={handleMouseUp}
> >
{/* Conteneur relatif pour positionner les bboxes et la sélection par rapport à l'image */} <img
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}> ref={imgRef}
<img src={b64ImgSrc(capture.screenshot_base64)}
ref={imgRef} alt="Capture plein ecran"
src={b64ImgSrc(capture.screenshot_base64)} draggable={false}
alt="Capture plein écran" onLoad={handleImageLoad}
draggable={false} style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }}
onLoad={handleImageLoad} />
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }}
/>
{/* Overlay des éléments détectés — visible dans tous les modes */} {/* Overlay : positionne exactement sur l'image via imageRect.
Tous les enfants sont positionnes relativement a ce div,
donc les coordonnees = coords_naturelles * imageScale. */}
<div
style={{
position: 'absolute',
left: imageRect.left,
top: imageRect.top,
width: imageRect.width,
height: imageRect.height,
pointerEvents: 'none',
}}
>
{/* Bboxes des elements detectes */}
{showDetection && detectedElements.map((elem) => ( {showDetection && detectedElements.map((elem) => (
<div <div
key={elem.id} key={elem.id}
@@ -569,6 +641,7 @@ function FullscreenSelector({
border: '2px solid #e94560', border: '2px solid #e94560',
background: 'rgba(233, 69, 96, 0.15)', background: 'rgba(233, 69, 96, 0.15)',
cursor: enabled ? 'pointer' : 'default', cursor: enabled ? 'pointer' : 'default',
pointerEvents: 'auto',
zIndex: 10, zIndex: 10,
}} }}
onClick={(e) => { onClick={(e) => {
@@ -593,8 +666,8 @@ function FullscreenSelector({
</div> </div>
))} ))}
{/* Zone de détection existante */} {/* Zone de detection existante */}
{detectionZone && imgRef.current && ( {detectionZone && (
<div <div
className="detection-zone-overlay" className="detection-zone-overlay"
style={{ style={{
@@ -606,7 +679,7 @@ function FullscreenSelector({
border: '3px solid #4caf50', border: '3px solid #4caf50',
background: 'rgba(76, 175, 80, 0.15)', background: 'rgba(76, 175, 80, 0.15)',
pointerEvents: 'none', pointerEvents: 'none',
zIndex: 5 zIndex: 5,
}} }}
> >
<span style={{ <span style={{
@@ -618,24 +691,23 @@ function FullscreenSelector({
padding: '2px 8px', padding: '2px 8px',
borderRadius: '3px', borderRadius: '3px',
fontSize: '11px', fontSize: '11px',
fontWeight: 'bold' fontWeight: 'bold',
}}> }}>
Zone de détection Zone de detection
</span> </span>
</div> </div>
)} )}
{/* Rectangle de sélection manuelle */} {/* Rectangle de selection manuelle */}
{(isSelecting || selection.width > 0) && ( {(isSelecting || selection.width > 0) && (
<div <div
ref={overlayRef}
className={`selection-overlay ${selectionMode === 'zone' ? 'zone-mode' : ''}`} className={`selection-overlay ${selectionMode === 'zone' ? 'zone-mode' : ''}`}
style={{ style={{
position: 'absolute', position: 'absolute',
left: selection.x, left: selection.x,
top: selection.y, top: selection.y,
width: selection.width, width: selection.width,
height: selection.height height: selection.height,
}} }}
/> />
)} )}