From 90c1d8036f4f3e711c90a8a78c50ef9f38c861fe Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 29 Apr 2026 18:20:16 +0200 Subject: [PATCH] =?UTF-8?q?ux(vwb):=20timer=20capture=20=E2=80=94=20defaul?= =?UTF-8?q?t=205s,=20label=20dynamique,=20log=20diagnostic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug terrain : le bouton 'Timer' déclenchait toujours une capture immédiate même après sélection d'un délai dans le menu déroulant. Le retour utilisateur 'le bouton ne change pas' a confirmé qu'il n'y avait aucun feedback visuel sur le délai sélectionné, donc impossible de diagnostiquer. Changements : - timerSeconds default 5s (préférence Dom) au lieu de 0 (Immediat) - Label dynamique du bouton : countdown actif → '5…' '4…' etc. délai 0 → 'Timer' (capture immédiate) délai > 0 → 'Capturer dans 5s' - Select préfixé par 'Délai :' pour clarifier - Conversion explicite String(timerSeconds) sur value du select pour éviter toute ambiguïté number/string - console.log temporaire au changement de select pour faciliter le diagnostic si le bug persiste (à retirer après validation) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/CapturePanel.tsx | 102 ++++++++++++++---- 1 file changed, 80 insertions(+), 22 deletions(-) diff --git a/visual_workflow_builder/frontend_v4/src/components/CapturePanel.tsx b/visual_workflow_builder/frontend_v4/src/components/CapturePanel.tsx index 2f9e2eed6..4c35e6652 100644 --- a/visual_workflow_builder/frontend_v4/src/components/CapturePanel.tsx +++ b/visual_workflow_builder/frontend_v4/src/components/CapturePanel.tsx @@ -4,7 +4,8 @@ import type { UIElement } from '../services/uiDetection'; import { loadLibraryAsync, saveLibrary, - compressThumbnail, + addCaptureToLibrary, + removeCaptureFromLibrary, } from '../services/captureLibraryStorage'; /** @@ -40,6 +41,8 @@ interface LibraryItem { timestamp: Date; sessionId?: string; favorite?: boolean; + format?: 'v2'; + fullImageUrl?: string; } export default function CapturePanel({ @@ -55,7 +58,7 @@ export default function CapturePanel({ const [showLibraryGallery, setShowLibraryGallery] = useState(false); const [library, setLibrary] = useState([]); const [currentCapture, setCurrentCapture] = useState(null); - const [timerSeconds, setTimerSeconds] = useState(0); + const [timerSeconds, setTimerSeconds] = useState(5); const [countdown, setCountdown] = useState(null); // Elements detectes sur l'apercu miniature const [previewElements, setPreviewElements] = useState([]); @@ -89,21 +92,27 @@ export default function CapturePanel({ } }, [library, libraryLoaded]); - // Ajouter capture a la bibliotheque (thumbnail compresse JPEG 320x240) + // Ajouter capture à la bibliothèque (format v2 : PNG HD côté backend, + // thumbnail 640x360 q85 dans le JSON pour la grille). useEffect(() => { if (!capture) return; setCurrentCapture(capture); let cancelled = false; (async () => { - const compressed = await compressThumbnail(capture.screenshot_base64); + const item = await addCaptureToLibrary(capture, { id: `cap_${Date.now()}` }); if (cancelled) return; - const newItem: LibraryItem = { - id: `cap_${Date.now()}`, - capture: { ...capture, screenshot_base64: compressed }, - timestamp: new Date(), - favorite: false, - }; - setLibrary(prev => [newItem, ...prev.slice(0, 19)]); + setLibrary(prev => [ + { + id: item.id, + capture: item.capture, + timestamp: typeof item.timestamp === 'string' ? new Date(item.timestamp) : item.timestamp, + sessionId: item.sessionId, + favorite: item.favorite ?? false, + format: item.format, + fullImageUrl: item.fullImageUrl, + }, + ...prev.slice(0, 19), + ]); })(); return () => { cancelled = true; }; }, [capture]); @@ -189,13 +198,44 @@ export default function CapturePanel({ }, 1000); }; - const handleLibrarySelect = (item: LibraryItem) => { - setCurrentCapture(item.capture); + const handleLibrarySelect = async (item: LibraryItem) => { + // Format v2 : remplacer le thumbnail par le PNG HD téléchargé du backend + // pour que la sélection d'ancre utilise une image non pixélisée. + if (item.format === 'v2' && item.fullImageUrl) { + try { + const resp = await fetch(item.fullImageUrl); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const blob = await resp.blob(); + const base64 = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const result = reader.result as string; + // FileReader → "data:image/png;base64,..." → on retire le préfixe + const idx = result.indexOf(','); + resolve(idx >= 0 ? result.slice(idx + 1) : result); + }; + reader.onerror = () => reject(reader.error); + reader.readAsDataURL(blob); + }); + setCurrentCapture({ ...item.capture, screenshot_base64: base64 }); + } catch (e) { + console.warn('[CaptureLibrary] Échec chargement HD, fallback thumbnail', e); + setCurrentCapture(item.capture); + } + } else { + setCurrentCapture(item.capture); + } setIsFullscreen(true); }; const handleDeleteLibraryItem = (id: string) => { + const target = library.find(it => it.id === id); setLibrary(prev => prev.filter(item => item.id !== id)); + // v2 : supprimer aussi le PNG côté backend (le saveLibrary auto-déclenché + // par le useEffect ne nettoie que le JSON, pas les fichiers PNG orphelins). + if (target?.format === 'v2') { + void removeCaptureFromLibrary(id, true); + } }; return ( @@ -204,17 +244,35 @@ export default function CapturePanel({ {/* Capture — auto-detection OS navigateur */}
- - -