Refonte majeure du système Agent Chat et ajout de nombreux modules : - Chat unifié : suppression du dual Workflows/Agent Libre, tout passe par /api/chat avec résolution en 3 niveaux (workflow → geste → "montre-moi") - GestureCatalog : 38 raccourcis clavier universels Windows avec matching sémantique, substitution automatique dans les replays, et endpoint /api/gestures - Mode Copilot : exécution pas-à-pas des workflows avec validation humaine via WebSocket (approve/skip/abort) avant chaque action - Léa UI (agent_v0/lea_ui/) : interface PyQt5 pour Windows avec overlay transparent pour feedback visuel pendant le replay - Data Extraction (core/extraction/) : moteur d'extraction visuelle de données (OCR + VLM → SQLite), avec schémas YAML et export CSV/Excel - ReplayVerifier (agent_v0/server_v1/) : vérification post-action par comparaison de screenshots, avec logique de retry (max 3) - IntentParser durci : meilleur fallback regex, type GREETING, patterns améliorés - Dashboard : nouvelles pages gestures, streaming, extractions - Tests : 63 tests GestureCatalog, 47 tests extraction, corrections tests existants - Dépréciation : /api/agent/plan et /api/agent/execute retournent HTTP 410, suppression du code hardcodé _plan_to_replay_actions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
361 lines
18 KiB
HTML
361 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>RPA Vision V3 - Streaming</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; }
|
|
.header { background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); color: white; padding: 20px 30px; display: flex; justify-content: space-between; align-items: center; }
|
|
.header h1 { font-size: 24px; display: flex; align-items: center; gap: 10px; }
|
|
.header .nav-links { display: flex; gap: 15px; align-items: center; }
|
|
.header .nav-links a { color: rgba(255,255,255,0.8); text-decoration: none; font-size: 14px; padding: 8px 16px; border-radius: 8px; transition: all 0.2s; }
|
|
.header .nav-links a:hover { background: rgba(255,255,255,0.15); color: white; }
|
|
.header .nav-links a.active { background: rgba(255,255,255,0.2); color: white; font-weight: 600; }
|
|
|
|
.container { max-width: 1600px; margin: 0 auto; padding: 20px; }
|
|
|
|
.page-intro { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border-radius: 12px; padding: 25px; border: 1px solid #334155; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; }
|
|
.page-intro div:first-child h2 { color: #e2e8f0; margin-bottom: 8px; font-size: 20px; }
|
|
.page-intro div:first-child p { color: #64748b; font-size: 14px; }
|
|
|
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 20px; }
|
|
.grid-2 { grid-template-columns: 1fr 1fr; }
|
|
.grid-4 { grid-template-columns: repeat(4, 1fr); }
|
|
|
|
.card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
|
|
.card h2 { font-size: 16px; margin-bottom: 15px; color: #94a3b8; display: flex; align-items: center; gap: 8px; }
|
|
.card h2 .icon { font-size: 20px; }
|
|
|
|
.stat-card { text-align: center; }
|
|
.stat-value { font-size: 36px; font-weight: bold; color: #3b82f6; }
|
|
.stat-label { font-size: 12px; color: #64748b; margin-top: 5px; text-transform: uppercase; }
|
|
|
|
.btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
|
|
.btn-primary { background: #3b82f6; color: white; }
|
|
.btn-primary:hover { background: #2563eb; }
|
|
.btn-success { background: #22c55e; color: white; }
|
|
.btn-success:hover { background: #16a34a; }
|
|
.btn-small { padding: 8px 16px; font-size: 12px; }
|
|
|
|
.live-stats { display: flex; flex-direction: column; gap: 12px; }
|
|
.stat-row { display: flex; justify-content: space-between; padding: 10px; background: #0f172a; border-radius: 6px; }
|
|
.stat-row span:first-child { color: #64748b; }
|
|
.stat-row span:last-child { font-weight: 600; color: #3b82f6; }
|
|
|
|
.session-list { max-height: 500px; overflow-y: auto; }
|
|
.session-item { display: flex; justify-content: space-between; align-items: center; padding: 15px; border-bottom: 1px solid #334155; transition: background 0.2s; }
|
|
.session-item:hover { background: #334155; }
|
|
.session-info h4 { color: #e2e8f0; margin-bottom: 5px; }
|
|
.session-info .meta { color: #64748b; font-size: 12px; }
|
|
.session-actions { display: flex; gap: 8px; }
|
|
|
|
.loading { text-align: center; padding: 40px; color: #64748b; }
|
|
.spinner { border: 3px solid #334155; border-top: 3px solid #3b82f6; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 0 auto 10px; }
|
|
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
|
|
.auto-refresh-indicator { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #64748b; }
|
|
.auto-refresh-indicator .dot { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; animation: pulse 2s infinite; }
|
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
|
|
.replay-progress { margin-top: 15px; }
|
|
.progress-bar { height: 10px; background: #334155; border-radius: 5px; overflow: hidden; margin-top: 8px; }
|
|
.progress-fill { height: 100%; background: linear-gradient(90deg, #3b82f6, #8b5cf6); border-radius: 5px; transition: width 0.5s ease; }
|
|
.progress-label { display: flex; justify-content: space-between; font-size: 12px; color: #64748b; margin-top: 5px; }
|
|
|
|
.gpu-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
|
|
.gpu-stat-item { background: #0f172a; border-radius: 8px; padding: 15px; text-align: center; }
|
|
.gpu-stat-item .label { font-size: 12px; color: #64748b; margin-bottom: 5px; }
|
|
.gpu-stat-item .value { font-size: 20px; font-weight: bold; color: #8b5cf6; }
|
|
|
|
@media (max-width: 768px) {
|
|
.grid-4 { grid-template-columns: 1fr 1fr; }
|
|
.grid-2 { grid-template-columns: 1fr; }
|
|
.header { flex-direction: column; gap: 15px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>RPA Vision V3 - Streaming</h1>
|
|
<div class="nav-links">
|
|
<a href="/">Dashboard</a>
|
|
<a href="/gestures">Gestes</a>
|
|
<a href="/streaming" class="active">Streaming</a>
|
|
<a href="/extractions">Extractions</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="page-intro">
|
|
<div>
|
|
<h2>Sessions de streaming temps reel</h2>
|
|
<p>Suivi des sessions de capture, replays en cours et statistiques du serveur de streaming (port 5005).</p>
|
|
</div>
|
|
<div style="display:flex;gap:10px;align-items:center;">
|
|
<div class="auto-refresh-indicator">
|
|
<span class="dot" id="refreshDot"></span>
|
|
<span id="refreshLabel">Auto-refresh: 5s</span>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="refreshAll()">Actualiser</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats globales -->
|
|
<div class="grid grid-4">
|
|
<div class="card stat-card">
|
|
<div class="stat-value" id="statActiveSessions">-</div>
|
|
<div class="stat-label">Sessions actives</div>
|
|
</div>
|
|
<div class="card stat-card">
|
|
<div class="stat-value" id="statTotalEvents">-</div>
|
|
<div class="stat-label">Evenements totaux</div>
|
|
</div>
|
|
<div class="card stat-card">
|
|
<div class="stat-value" id="statWorkflowsBuilt">-</div>
|
|
<div class="stat-label">Workflows construits</div>
|
|
</div>
|
|
<div class="card stat-card">
|
|
<div id="statServerStatus" style="font-size:24px;">⏳</div>
|
|
<div class="stat-label">Serveur streaming</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-2">
|
|
<!-- Sessions actives -->
|
|
<div class="card">
|
|
<h2><span class="icon">🎬</span> Sessions actives</h2>
|
|
<div id="sessionsList" class="session-list">
|
|
<div class="loading"><div class="spinner"></div>Chargement...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Replay en cours -->
|
|
<div class="card">
|
|
<h2><span class="icon">▶️</span> Replay en cours</h2>
|
|
<div id="replayStatus">
|
|
<div class="loading"><div class="spinner"></div>Chargement...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-2">
|
|
<!-- Stats GPU -->
|
|
<div class="card">
|
|
<h2><span class="icon">💻</span> Ressources GPU</h2>
|
|
<div id="gpuStats">
|
|
<div class="loading"><div class="spinner"></div>Chargement...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Details serveur -->
|
|
<div class="card">
|
|
<h2><span class="icon">📊</span> Statistiques serveur</h2>
|
|
<div class="live-stats" id="serverDetails">
|
|
<div class="loading"><div class="spinner"></div>Chargement...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const STREAMING_API = '/api/streaming/status';
|
|
|
|
async function fetchJSON(url) {
|
|
const response = await fetch(url);
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
return response.json();
|
|
}
|
|
|
|
async function refreshAll() {
|
|
await Promise.all([
|
|
refreshStats(),
|
|
refreshSessions(),
|
|
refreshReplay(),
|
|
refreshGPU()
|
|
]);
|
|
}
|
|
|
|
async function refreshStats() {
|
|
const statusEl = document.getElementById('statServerStatus');
|
|
const detailsEl = document.getElementById('serverDetails');
|
|
|
|
try {
|
|
const data = await fetchJSON(STREAMING_API);
|
|
|
|
statusEl.innerHTML = '<span style="color:#22c55e;">✅</span>';
|
|
statusEl.title = 'Serveur streaming en ligne';
|
|
|
|
document.getElementById('statActiveSessions').textContent = data.active_sessions || 0;
|
|
document.getElementById('statTotalEvents').textContent = data.total_events || 0;
|
|
document.getElementById('statWorkflowsBuilt').textContent = data.workflows_built || 0;
|
|
|
|
// Details serveur
|
|
const rows = [];
|
|
if (data.uptime !== undefined) rows.push({label: 'Uptime', value: formatUptime(data.uptime)});
|
|
if (data.total_sessions !== undefined) rows.push({label: 'Sessions totales', value: data.total_sessions});
|
|
if (data.active_sessions !== undefined) rows.push({label: 'Sessions actives', value: data.active_sessions});
|
|
if (data.total_events !== undefined) rows.push({label: 'Evenements totaux', value: data.total_events});
|
|
if (data.workflows_built !== undefined) rows.push({label: 'Workflows construits', value: data.workflows_built});
|
|
if (data.events_per_second !== undefined) rows.push({label: 'Evenements/sec', value: (data.events_per_second || 0).toFixed(2)});
|
|
if (data.memory_usage_mb !== undefined) rows.push({label: 'Memoire utilisee', value: Math.round(data.memory_usage_mb) + ' MB'});
|
|
if (data.server_version) rows.push({label: 'Version serveur', value: data.server_version});
|
|
|
|
if (rows.length === 0) {
|
|
const rawRows = Object.entries(data).map(([k, v]) => ({label: k, value: JSON.stringify(v)}));
|
|
detailsEl.innerHTML = rawRows.map(r =>
|
|
`<div class="stat-row"><span>${r.label}</span><span>${r.value}</span></div>`
|
|
).join('');
|
|
} else {
|
|
detailsEl.innerHTML = rows.map(r =>
|
|
`<div class="stat-row"><span>${r.label}</span><span>${r.value}</span></div>`
|
|
).join('');
|
|
}
|
|
|
|
} catch (e) {
|
|
statusEl.innerHTML = '<span style="color:#ef4444;">❌</span>';
|
|
statusEl.title = 'Serveur streaming hors ligne';
|
|
document.getElementById('statActiveSessions').textContent = '-';
|
|
document.getElementById('statTotalEvents').textContent = '-';
|
|
document.getElementById('statWorkflowsBuilt').textContent = '-';
|
|
detailsEl.innerHTML = `<div style="text-align:center;padding:20px;color:#ef4444;">
|
|
Serveur streaming inaccessible (port 5005)<br>
|
|
<span style="font-size:12px;color:#94a3b8;margin-top:5px;display:block;">${e.message}</span>
|
|
</div>`;
|
|
}
|
|
}
|
|
|
|
async function refreshSessions() {
|
|
const list = document.getElementById('sessionsList');
|
|
try {
|
|
const data = await fetchJSON('/api/streaming/status');
|
|
const sessions = data.sessions || [];
|
|
|
|
if (!Array.isArray(sessions) || sessions.length === 0) {
|
|
list.innerHTML = '<p style="text-align:center;color:#64748b;padding:30px;">Aucune session de streaming active</p>';
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = sessions.map(s => {
|
|
const sessionId = s.session_id || s.id || 'N/A';
|
|
const status = s.status || 'inconnu';
|
|
const events = s.events_count || s.total_events || 0;
|
|
const screenshots = s.screenshots_count || 0;
|
|
const started = s.started_at ? new Date(s.started_at).toLocaleString('fr-FR') : '-';
|
|
const duration = s.duration_seconds ? formatUptime(s.duration_seconds) : '-';
|
|
const statusColor = status === 'active' ? '#22c55e' : (status === 'completed' ? '#3b82f6' : '#64748b');
|
|
|
|
return `
|
|
<div class="session-item">
|
|
<div class="session-info">
|
|
<h4>
|
|
${sessionId}
|
|
<span style="margin-left:10px;padding:3px 8px;border-radius:4px;font-size:12px;background:rgba(${statusColor === '#22c55e' ? '34,197,94' : '59,130,246'},0.15);color:${statusColor};">
|
|
${status}
|
|
</span>
|
|
</h4>
|
|
<div class="meta">Debut: ${started} | Duree: ${duration} | ${events} evenements | ${screenshots} screenshots</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
} catch (e) {
|
|
list.innerHTML = `<p style="color:#ef4444;text-align:center;padding:20px;">Erreur: ${e.message}</p>`;
|
|
}
|
|
}
|
|
|
|
async function refreshReplay() {
|
|
const container = document.getElementById('replayStatus');
|
|
try {
|
|
const data = await fetchJSON('/api/streaming/status');
|
|
const replay = data.replay || null;
|
|
|
|
if (!replay || !replay.active) {
|
|
container.innerHTML = '<p style="text-align:center;color:#64748b;padding:30px;">Aucun replay en cours</p>';
|
|
return;
|
|
}
|
|
|
|
const progress = replay.progress || 0;
|
|
const total = replay.total_actions || 0;
|
|
const completed = replay.completed_actions || 0;
|
|
const pct = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
|
|
container.innerHTML = `
|
|
<div class="live-stats">
|
|
<div class="stat-row"><span>Workflow</span><span>${replay.workflow_id || '-'}</span></div>
|
|
<div class="stat-row"><span>Statut</span><span style="color:#22c55e;">En cours</span></div>
|
|
<div class="stat-row"><span>Actions</span><span>${completed} / ${total}</span></div>
|
|
</div>
|
|
<div class="replay-progress">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" style="width:${pct}%"></div>
|
|
</div>
|
|
<div class="progress-label">
|
|
<span>${pct}% complete</span>
|
|
<span>${completed} / ${total} actions</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} catch (e) {
|
|
container.innerHTML = '<p style="text-align:center;color:#64748b;padding:30px;">Aucun replay en cours</p>';
|
|
}
|
|
}
|
|
|
|
async function refreshGPU() {
|
|
const container = document.getElementById('gpuStats');
|
|
try {
|
|
const data = await fetchJSON('/api/streaming/status');
|
|
const gpu = data.gpu || null;
|
|
|
|
if (!gpu) {
|
|
container.innerHTML = '<p style="text-align:center;color:#64748b;padding:30px;">Statistiques GPU non disponibles</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<div class="gpu-stats">
|
|
<div class="gpu-stat-item">
|
|
<div class="label">GPU</div>
|
|
<div class="value">${gpu.name || 'N/A'}</div>
|
|
</div>
|
|
<div class="gpu-stat-item">
|
|
<div class="label">Memoire utilisee</div>
|
|
<div class="value">${gpu.memory_used_mb ? Math.round(gpu.memory_used_mb) + ' MB' : 'N/A'}</div>
|
|
</div>
|
|
<div class="gpu-stat-item">
|
|
<div class="label">Memoire totale</div>
|
|
<div class="value">${gpu.memory_total_mb ? Math.round(gpu.memory_total_mb) + ' MB' : 'N/A'}</div>
|
|
</div>
|
|
<div class="gpu-stat-item">
|
|
<div class="label">Utilisation</div>
|
|
<div class="value">${gpu.utilization !== undefined ? gpu.utilization + '%' : 'N/A'}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} catch (e) {
|
|
container.innerHTML = '<p style="text-align:center;color:#64748b;padding:30px;">Statistiques GPU non disponibles</p>';
|
|
}
|
|
}
|
|
|
|
function formatUptime(seconds) {
|
|
if (!seconds && seconds !== 0) return '-';
|
|
const h = Math.floor(seconds / 3600);
|
|
const m = Math.floor((seconds % 3600) / 60);
|
|
const s = Math.floor(seconds % 60);
|
|
if (h > 0) return `${h}h ${m}m`;
|
|
if (m > 0) return `${m}m ${s}s`;
|
|
return `${s}s`;
|
|
}
|
|
|
|
// Initial load
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
refreshAll();
|
|
// Auto-refresh toutes les 5 secondes
|
|
setInterval(refreshAll, 5000);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|