feat: capture Windows auto-détection OS, chat Léa agrandi, UX améliorée

- Capture auto : détecte OS navigateur → capture Windows ou Linux
- Timer capture utilise aussi la smart capture
- Heartbeat background permanent (même sans session)
- Tri screenshots par date (plus de vieilles captures)
- Chat Léa : 450x650, polices 11pt, redimensionnable, meilleur contraste
- Bouton Exécuter : "Linux" + "Windows" avec feedback visuel
- Délai 5s avant replay Windows (temps de réduire le navigateur)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-03-17 23:03:53 +01:00
parent 8175b39eba
commit 4e217e30dd
4 changed files with 81 additions and 58 deletions

View File

@@ -111,9 +111,30 @@ export default function CapturePanel({
}
};
// Capture intelligente — auto-détection OS
const doSmartCapture = async () => {
const isWindows = navigator.platform?.includes('Win') || navigator.userAgent?.includes('Windows');
if (isWindows) {
try {
const resp = await fetch('/api/screen-capture/capture-windows', { method: 'POST' });
const data = await resp.json();
if (data.image) {
setCurrentCapture({
screenshot_base64: data.image,
width: data.width,
height: data.height,
source: 'windows',
} as any);
}
} catch {}
} else {
onCapture();
}
};
const handleTimerCapture = () => {
if (timerSeconds === 0) {
onCapture();
doSmartCapture();
return;
}
@@ -127,7 +148,7 @@ export default function CapturePanel({
} else {
clearInterval(interval);
setCountdown(null);
onCapture();
doSmartCapture();
}
}, 1000);
};
@@ -145,33 +166,10 @@ export default function CapturePanel({
<div className="capture-panel">
<h3>Capture</h3>
{/* Contrôles de capture */}
{/* Capture — auto-détection OS navigateur */}
<div className="capture-controls">
<button onClick={onCapture} disabled={countdown !== null}>
Capturer
</button>
<button
onClick={async () => {
try {
const resp = await fetch('/api/screen-capture/capture-windows', { method: 'POST' });
const data = await resp.json();
if (data.image) {
const fakeCapture = {
screenshot_base64: data.image,
width: data.width,
height: data.height,
source: 'windows',
};
setCurrentCapture(fakeCapture as any);
}
} catch (err) {
console.error('Capture Windows échouée:', err);
}
}}
title="Capture le dernier écran du PC Windows"
style={{ fontSize: '12px' }}
>
🖥 Windows
<button disabled={countdown !== null} onClick={doSmartCapture}>
📸 Capturer
</button>
<select value={timerSeconds} onChange={(e) => setTimerSeconds(Number(e.target.value))}>
<option value="0">Immédiat</option>

View File

@@ -1,4 +1,5 @@
// @ts-nocheck
import { useState } from 'react';
import type { Execution } from '../types';
interface Props {
@@ -9,20 +10,19 @@ interface Props {
export default function ExecutionControls({ execution, onStart, onStop }: Props) {
const isRunning = execution?.status === 'running' || execution?.status === 'paused';
const [windowsStatus, setWindowsStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle');
const handleExecuteWindows = async () => {
setWindowsStatus('sending');
try {
// Récupérer le workflow actif depuis l'état de la session
const stateResp = await fetch('/api/v3/session/state');
const state = await stateResp.json();
let workflowId = state?.session?.active_workflow_id;
let steps = state?.workflow?.steps || [];
// Si pas de workflow actif, essayer de charger le premier disponible
if (!steps.length && state?.workflows_list?.length) {
const firstWf = state.workflows_list[0];
workflowId = firstWf.id;
// Charger les étapes du workflow
const wfResp = await fetch(`/api/v3/workflow/${firstWf.id}`);
const wfData = await wfResp.json();
steps = wfData?.steps || wfData?.workflow?.steps || [];
@@ -30,10 +30,10 @@ export default function ExecutionControls({ execution, onStart, onStop }: Props)
if (!steps.length) {
alert('Aucune étape dans le workflow. Sélectionnez un workflow d\'abord.');
setWindowsStatus('idle');
return;
}
// Via le proxy Vite (/api → port 5002)
const resp = await fetch('/api/v3/execute-windows', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -52,36 +52,50 @@ export default function ExecutionControls({ execution, onStart, onStop }: Props)
const result = await resp.json();
if (result.replay_id) {
alert(`Replay lancé sur Windows ! ID: ${result.replay_id}`);
setWindowsStatus('sent');
alert('Replay lancé ! Réduisez cette fenêtre maintenant.\nLes actions commenceront dans 5 secondes.');
setTimeout(() => setWindowsStatus('idle'), 5000);
} else {
alert(`Erreur: ${result.error || JSON.stringify(result)}`);
setWindowsStatus('error');
setTimeout(() => setWindowsStatus('idle'), 3000);
}
} catch (err) {
alert(`Erreur connexion streaming server: ${err}`);
alert(`Erreur connexion: ${err}`);
setWindowsStatus('error');
setTimeout(() => setWindowsStatus('idle'), 3000);
}
};
return (
<div className="execution-controls">
{!isRunning ? (
<div style={{ display: 'flex', gap: '4px' }}>
<button className="btn-start" onClick={onStart}>
Exécuter
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
<button className="btn-start" onClick={onStart} title="Exécuter sur cet écran (Linux)">
🐧 Linux
</button>
<button
className="btn-start"
onClick={handleExecuteWindows}
style={{ background: '#0078d4', fontSize: '12px' }}
title="Envoyer les actions au PC Windows via le streaming server"
disabled={windowsStatus === 'sending'}
style={{
background: windowsStatus === 'sent' ? '#22c55e' : windowsStatus === 'error' ? '#ef4444' : '#0078d4',
fontSize: '12px',
opacity: windowsStatus === 'sending' ? 0.6 : 1,
}}
title="Exécuter sur le PC Windows distant"
>
🖥 Windows
{windowsStatus === 'sending' ? '⏳ Envoi...' :
windowsStatus === 'sent' ? '✅ Lancé !' :
windowsStatus === 'error' ? '❌ Erreur' :
'🖥️ Windows'}
</button>
</div>
) : (
<>
<div className="exec-status">
<span className={`status-badge ${execution?.status}`}>
{execution?.status === 'running' ? 'En cours' : 'En pause'}
{execution?.status === 'running' ? 'En cours' : '⏸️ En pause'}
</span>
<span className="exec-progress">
{execution?.completed_steps}/{execution?.total_steps}
@@ -93,10 +107,6 @@ export default function ExecutionControls({ execution, onStart, onStop }: Props)
</>
)}
{execution?.status === 'completed' && (
<span className="status-badge completed">Terminé</span>
)}
{execution?.status === 'error' && (
<span className="status-badge error" title={execution.error_message}>
Erreur