feat: chat unifié, GestureCatalog, Copilot, Léa UI, extraction données, vérification replay
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>
This commit is contained in:
@@ -2085,6 +2085,269 @@ def import_config():
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Catalogue de gestes primitifs (intégré)
|
||||
# =============================================================================
|
||||
|
||||
# Catalogue intégré des gestes primitifs connus par le moteur RPA
|
||||
_BUILTIN_GESTURE_CATALOG = [
|
||||
# Fenêtres
|
||||
{"name": "Fermer la fenêtre", "shortcut": "Alt+F4", "category": "windows", "icon": "❌",
|
||||
"description": "Ferme la fenêtre active ou l'application en cours."},
|
||||
{"name": "Agrandir la fenêtre", "shortcut": "Win+Up", "category": "windows", "icon": "⬆️",
|
||||
"description": "Agrandit la fenêtre active en plein écran."},
|
||||
{"name": "Réduire la fenêtre", "shortcut": "Win+Down", "category": "windows", "icon": "⬇️",
|
||||
"description": "Réduit ou restaure la fenêtre active."},
|
||||
{"name": "Fenêtre à gauche", "shortcut": "Win+Left", "category": "windows", "icon": "⬅️",
|
||||
"description": "Ancre la fenêtre active sur la moitié gauche de l'écran."},
|
||||
{"name": "Fenêtre à droite", "shortcut": "Win+Right", "category": "windows", "icon": "➡️",
|
||||
"description": "Ancre la fenêtre active sur la moitié droite de l'écran."},
|
||||
{"name": "Changer de fenêtre", "shortcut": "Alt+Tab", "category": "windows", "icon": "🔄",
|
||||
"description": "Bascule entre les fenêtres ouvertes."},
|
||||
{"name": "Bureau", "shortcut": "Win+D", "category": "windows", "icon": "🖥️",
|
||||
"description": "Affiche le bureau (réduit toutes les fenêtres)."},
|
||||
# Navigation Chrome
|
||||
{"name": "Nouvel onglet", "shortcut": "Ctrl+T", "category": "chrome", "icon": "➕",
|
||||
"description": "Ouvre un nouvel onglet dans le navigateur."},
|
||||
{"name": "Fermer l'onglet", "shortcut": "Ctrl+W", "category": "chrome", "icon": "✖️",
|
||||
"description": "Ferme l'onglet actif du navigateur."},
|
||||
{"name": "Onglet précédent", "shortcut": "Ctrl+Shift+Tab", "category": "chrome", "icon": "◀️",
|
||||
"description": "Passe à l'onglet précédent."},
|
||||
{"name": "Onglet suivant", "shortcut": "Ctrl+Tab", "category": "chrome", "icon": "▶️",
|
||||
"description": "Passe à l'onglet suivant."},
|
||||
{"name": "Rouvrir onglet fermé", "shortcut": "Ctrl+Shift+T", "category": "chrome", "icon": "♻️",
|
||||
"description": "Rouvre le dernier onglet fermé."},
|
||||
{"name": "Barre d'adresse", "shortcut": "Ctrl+L", "category": "chrome", "icon": "🔗",
|
||||
"description": "Sélectionne la barre d'adresse."},
|
||||
{"name": "Rechercher dans la page", "shortcut": "Ctrl+F", "category": "chrome", "icon": "🔍",
|
||||
"description": "Ouvre la barre de recherche dans la page."},
|
||||
{"name": "Actualiser la page", "shortcut": "F5", "category": "chrome", "icon": "🔄",
|
||||
"description": "Recharge la page courante."},
|
||||
{"name": "Page précédente", "shortcut": "Alt+Left", "category": "chrome", "icon": "⏪",
|
||||
"description": "Retourne à la page précédente dans l'historique."},
|
||||
{"name": "Page suivante", "shortcut": "Alt+Right", "category": "chrome", "icon": "⏩",
|
||||
"description": "Avance à la page suivante dans l'historique."},
|
||||
# Édition
|
||||
{"name": "Copier", "shortcut": "Ctrl+C", "category": "edition", "icon": "📋",
|
||||
"description": "Copie la sélection dans le presse-papiers."},
|
||||
{"name": "Coller", "shortcut": "Ctrl+V", "category": "edition", "icon": "📌",
|
||||
"description": "Colle le contenu du presse-papiers."},
|
||||
{"name": "Couper", "shortcut": "Ctrl+X", "category": "edition", "icon": "✂️",
|
||||
"description": "Coupe la sélection et la place dans le presse-papiers."},
|
||||
{"name": "Annuler", "shortcut": "Ctrl+Z", "category": "edition", "icon": "↩️",
|
||||
"description": "Annule la dernière action."},
|
||||
{"name": "Rétablir", "shortcut": "Ctrl+Y", "category": "edition", "icon": "↪️",
|
||||
"description": "Rétablit la dernière action annulée."},
|
||||
{"name": "Tout sélectionner", "shortcut": "Ctrl+A", "category": "edition", "icon": "📝",
|
||||
"description": "Sélectionne tout le contenu."},
|
||||
{"name": "Enregistrer", "shortcut": "Ctrl+S", "category": "edition", "icon": "💾",
|
||||
"description": "Enregistre le document en cours."},
|
||||
{"name": "Imprimer", "shortcut": "Ctrl+P", "category": "edition", "icon": "🖨️",
|
||||
"description": "Ouvre la boîte de dialogue d'impression."},
|
||||
# Système
|
||||
{"name": "Menu Démarrer", "shortcut": "Win", "category": "system", "icon": "🪟",
|
||||
"description": "Ouvre le menu Démarrer de Windows."},
|
||||
{"name": "Explorateur de fichiers", "shortcut": "Win+E", "category": "system", "icon": "📁",
|
||||
"description": "Ouvre l'explorateur de fichiers."},
|
||||
{"name": "Gestionnaire de tâches", "shortcut": "Ctrl+Shift+Esc", "category": "system", "icon": "📊",
|
||||
"description": "Ouvre le gestionnaire de tâches."},
|
||||
{"name": "Paramètres", "shortcut": "Win+I", "category": "system", "icon": "⚙️",
|
||||
"description": "Ouvre les paramètres Windows."},
|
||||
{"name": "Verrouiller l'écran", "shortcut": "Win+L", "category": "system", "icon": "🔒",
|
||||
"description": "Verrouille la session utilisateur."},
|
||||
{"name": "Capture d'écran", "shortcut": "Win+Shift+S", "category": "system", "icon": "📷",
|
||||
"description": "Lance l'outil de capture d'écran."},
|
||||
{"name": "Exécuter", "shortcut": "Win+R", "category": "system", "icon": "▶️",
|
||||
"description": "Ouvre la boîte de dialogue Exécuter."},
|
||||
# Actions souris
|
||||
{"name": "Clic gauche", "shortcut": "", "category": "mouse", "icon": "🖱️",
|
||||
"description": "Clic gauche sur un élément de l'interface."},
|
||||
{"name": "Double clic", "shortcut": "", "category": "mouse", "icon": "🖱️",
|
||||
"description": "Double clic gauche pour ouvrir ou sélectionner."},
|
||||
{"name": "Clic droit", "shortcut": "", "category": "mouse", "icon": "🖱️",
|
||||
"description": "Clic droit pour ouvrir le menu contextuel."},
|
||||
{"name": "Glisser-déposer", "shortcut": "", "category": "mouse", "icon": "✋",
|
||||
"description": "Maintenir le clic et déplacer un élément."},
|
||||
{"name": "Scroll (molette)", "shortcut": "", "category": "mouse", "icon": "🔃",
|
||||
"description": "Défilement vertical avec la molette de la souris."},
|
||||
]
|
||||
|
||||
_GESTURE_CATEGORIES = {
|
||||
"windows": {"id": "windows", "name": "Gestion des fenêtres", "icon": "🪟"},
|
||||
"chrome": {"id": "chrome", "name": "Navigation Chrome / Web", "icon": "🌐"},
|
||||
"edition": {"id": "edition", "name": "Édition de texte", "icon": "✏️"},
|
||||
"system": {"id": "system", "name": "Système", "icon": "⚙️"},
|
||||
"mouse": {"id": "mouse", "name": "Actions souris", "icon": "🖱️"},
|
||||
}
|
||||
|
||||
|
||||
def _get_gestures():
|
||||
"""Récupère les gestes depuis le module agent_chat ou le catalogue intégré."""
|
||||
catalog_available = False
|
||||
gestures = []
|
||||
|
||||
try:
|
||||
from agent_chat.gesture_catalog import get_all_gestures
|
||||
gestures = get_all_gestures()
|
||||
catalog_available = True
|
||||
except (ImportError, AttributeError):
|
||||
gestures = _BUILTIN_GESTURE_CATALOG
|
||||
catalog_available = False
|
||||
|
||||
return gestures, catalog_available
|
||||
|
||||
|
||||
def _build_gesture_categories(gestures):
|
||||
"""Organise les gestes par catégorie pour le template."""
|
||||
categories_map = {}
|
||||
for g in gestures:
|
||||
cat_id = g.get('category', 'other')
|
||||
if cat_id not in categories_map:
|
||||
cat_info = _GESTURE_CATEGORIES.get(cat_id, {
|
||||
"id": cat_id, "name": cat_id.capitalize(), "icon": "📦"
|
||||
})
|
||||
categories_map[cat_id] = {**cat_info, "gestures": []}
|
||||
categories_map[cat_id]["gestures"].append(g)
|
||||
|
||||
# Ordre déterministe
|
||||
order = ["windows", "chrome", "edition", "system", "mouse"]
|
||||
result = []
|
||||
for cat_id in order:
|
||||
if cat_id in categories_map:
|
||||
result.append(categories_map.pop(cat_id))
|
||||
# Catégories restantes
|
||||
for cat_id in sorted(categories_map.keys()):
|
||||
result.append(categories_map[cat_id])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@app.route('/gestures')
|
||||
def gestures_page():
|
||||
"""Page listant tous les gestes primitifs connus."""
|
||||
gestures, available = _get_gestures()
|
||||
categories = _build_gesture_categories(gestures)
|
||||
with_shortcut = sum(1 for g in gestures if g.get('shortcut'))
|
||||
|
||||
stats = {
|
||||
'total': len(gestures),
|
||||
'categories': len(categories),
|
||||
'with_shortcut': with_shortcut,
|
||||
'source': 'Module' if available else 'Intégré',
|
||||
}
|
||||
|
||||
return render_template('gestures.html',
|
||||
categories=categories,
|
||||
stats=stats,
|
||||
available=available)
|
||||
|
||||
|
||||
@app.route('/api/gestures')
|
||||
def api_gestures():
|
||||
"""API JSON des gestes primitifs."""
|
||||
gestures, available = _get_gestures()
|
||||
categories = _build_gesture_categories(gestures)
|
||||
return jsonify({
|
||||
'gestures': gestures,
|
||||
'categories': [{'id': c['id'], 'name': c['name'], 'count': len(c['gestures'])}
|
||||
for c in categories],
|
||||
'total': len(gestures),
|
||||
'source': 'agent_chat.gesture_catalog' if available else 'builtin',
|
||||
'available': available,
|
||||
})
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Pages Streaming et Extractions
|
||||
# =============================================================================
|
||||
|
||||
@app.route('/streaming')
|
||||
def streaming_page():
|
||||
"""Page montrant l'état des sessions de streaming."""
|
||||
return render_template('streaming.html')
|
||||
|
||||
|
||||
@app.route('/api/streaming/status')
|
||||
def api_streaming_status():
|
||||
"""Statut en temps réel du streaming server."""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
streaming_url = 'http://localhost:5005/api/v1/traces/stream/stats'
|
||||
try:
|
||||
req = urllib.request.Request(streaming_url, headers={'Accept': 'application/json'})
|
||||
with urllib.request.urlopen(req, timeout=5) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
return jsonify(data)
|
||||
except urllib.error.URLError:
|
||||
return jsonify({
|
||||
'error': 'Serveur streaming inaccessible',
|
||||
'active_sessions': 0,
|
||||
'total_events': 0,
|
||||
'workflows_built': 0,
|
||||
}), 502
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/extractions')
|
||||
def extractions_page():
|
||||
"""Page listant les extractions de données."""
|
||||
extraction_available = False
|
||||
extractions = []
|
||||
stats = {'total': 0, 'total_records': 0, 'schemas': 0, 'last_extraction': None}
|
||||
|
||||
try:
|
||||
from core.extraction import get_extractions
|
||||
raw = get_extractions()
|
||||
extractions = raw.get('extractions', [])
|
||||
stats = raw.get('stats', stats)
|
||||
extraction_available = True
|
||||
except (ImportError, AttributeError):
|
||||
extraction_available = False
|
||||
|
||||
return render_template('extractions.html',
|
||||
extractions=extractions,
|
||||
stats=stats,
|
||||
available=extraction_available)
|
||||
|
||||
|
||||
@app.route('/api/extractions')
|
||||
def api_extractions():
|
||||
"""API JSON des extractions de données."""
|
||||
try:
|
||||
from core.extraction import get_extractions
|
||||
data = get_extractions()
|
||||
return jsonify({**data, 'available': True})
|
||||
except (ImportError, AttributeError):
|
||||
return jsonify({
|
||||
'available': False,
|
||||
'extractions': [],
|
||||
'message': 'Module core.extraction non disponible',
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/extractions/<extraction_id>/export')
|
||||
def api_extraction_export(extraction_id):
|
||||
"""Export d'une extraction au format CSV."""
|
||||
try:
|
||||
from core.extraction import export_extraction
|
||||
fmt = request.args.get('format', 'csv')
|
||||
result = export_extraction(extraction_id, fmt)
|
||||
if result is None:
|
||||
return jsonify({'error': 'Extraction non trouvée'}), 404
|
||||
return send_file(
|
||||
result['path'],
|
||||
as_attachment=True,
|
||||
download_name=result.get('filename', f'extraction_{extraction_id}.csv'),
|
||||
)
|
||||
except (ImportError, AttributeError):
|
||||
return jsonify({
|
||||
'error': 'Module core.extraction non disponible',
|
||||
}), 501
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# API Streaming - Proxy vers le serveur de streaming (port 5005)
|
||||
# =============================================================================
|
||||
|
||||
@@ -22,9 +22,9 @@ if [ ! -f "web_dashboard/app.py" ]; then
|
||||
fi
|
||||
|
||||
# Activer l'environnement virtuel
|
||||
if [ -d "../geniusia2/venv" ]; then
|
||||
if [ -d ".venv" ]; then
|
||||
echo -e "${YELLOW}[1/3]${NC} Activation de l'environnement virtuel..."
|
||||
source ../geniusia2/venv/bin/activate
|
||||
source .venv/bin/activate
|
||||
echo -e "${GREEN}✓${NC} Environnement activé"
|
||||
elif [ -d "venv_v3" ]; then
|
||||
echo -e "${YELLOW}[1/3]${NC} Activation de l'environnement virtuel..."
|
||||
@@ -52,7 +52,7 @@ mkdir -p logs
|
||||
echo -e "${YELLOW}[3/3]${NC} Lancement du dashboard..."
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Dashboard disponible sur: http://localhost:5000 ║"
|
||||
echo "║ Dashboard disponible sur: http://localhost:5001 ║"
|
||||
echo "║ Appuyez sur Ctrl+C pour arrêter ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
214
web_dashboard/templates/extractions.html
Normal file
214
web_dashboard/templates/extractions.html
Normal file
@@ -0,0 +1,214 @@
|
||||
<!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 - Extractions</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-warning { background: #f59e0b; color: white; }
|
||||
.btn-warning:hover { background: #d97706; }
|
||||
.btn-small { padding: 8px 16px; font-size: 12px; }
|
||||
|
||||
.extraction-list { display: flex; flex-direction: column; gap: 12px; }
|
||||
.extraction-item { display: flex; justify-content: space-between; align-items: center; padding: 20px; background: #0f172a; border-radius: 10px; border: 1px solid #334155; transition: all 0.2s; }
|
||||
.extraction-item:hover { border-color: #3b82f6; }
|
||||
.extraction-main { display: flex; align-items: center; gap: 15px; flex: 1; }
|
||||
.extraction-icon { font-size: 32px; }
|
||||
.extraction-details h4 { color: #e2e8f0; margin-bottom: 5px; }
|
||||
.extraction-details p { color: #64748b; font-size: 12px; }
|
||||
.extraction-stats { display: flex; gap: 20px; }
|
||||
.extraction-stat { text-align: center; }
|
||||
.extraction-stat .value { font-size: 18px; font-weight: bold; color: #3b82f6; }
|
||||
.extraction-stat .label { font-size: 10px; color: #64748b; text-transform: uppercase; }
|
||||
.extraction-actions { display: flex; gap: 8px; margin-left: 20px; }
|
||||
|
||||
.unavailable-msg { text-align: center; padding: 60px 20px; color: #64748b; }
|
||||
.unavailable-msg .msg-icon { font-size: 48px; margin-bottom: 15px; }
|
||||
.unavailable-msg h3 { color: #94a3b8; margin-bottom: 10px; }
|
||||
.unavailable-msg p { font-size: 14px; max-width: 500px; margin: 0 auto; }
|
||||
|
||||
.empty-state { text-align: center; padding: 60px 20px; }
|
||||
.empty-state .empty-icon { font-size: 64px; margin-bottom: 15px; opacity: 0.5; }
|
||||
.empty-state h3 { color: #94a3b8; margin-bottom: 10px; font-size: 18px; }
|
||||
.empty-state p { color: #64748b; font-size: 14px; max-width: 400px; margin: 0 auto; }
|
||||
|
||||
.status-badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; }
|
||||
.status-badge.completed { background: #052e16; color: #22c55e; }
|
||||
.status-badge.running { background: #172554; color: #3b82f6; }
|
||||
.status-badge.failed { background: #450a0a; color: #ef4444; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-4 { grid-template-columns: 1fr 1fr; }
|
||||
.grid-2 { grid-template-columns: 1fr; }
|
||||
.header { flex-direction: column; gap: 15px; }
|
||||
.extraction-item { flex-direction: column; gap: 15px; align-items: flex-start; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>RPA Vision V3 - Extractions</h1>
|
||||
<div class="nav-links">
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/gestures">Gestes</a>
|
||||
<a href="/streaming">Streaming</a>
|
||||
<a href="/extractions" class="active">Extractions</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="page-intro">
|
||||
<div>
|
||||
<h2>Extractions de donnees</h2>
|
||||
<p>Visualisation des donnees extraites par le moteur RPA depuis les ecrans des applications (scraping visuel).</p>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-primary" onclick="refreshExtractions()">Actualiser</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not available %}
|
||||
<div class="card">
|
||||
<div class="unavailable-msg">
|
||||
<div class="msg-icon">⚠️</div>
|
||||
<h3>Module non disponible</h3>
|
||||
<p>Le module <code>core.extraction</code> n'est pas encore installe. Cette fonctionnalite sera disponible dans une prochaine version.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-4">
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value" id="statTotalExtractions">{{ stats.total }}</div>
|
||||
<div class="stat-label">Extractions</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value" id="statTotalRecords">{{ stats.total_records }}</div>
|
||||
<div class="stat-label">Enregistrements</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value" id="statSchemas">{{ stats.schemas }}</div>
|
||||
<div class="stat-label">Schemas</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value" id="statLastExtraction">{{ stats.last_extraction or '-' }}</div>
|
||||
<div class="stat-label">Derniere extraction</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Liste des extractions -->
|
||||
<div class="card">
|
||||
<h2><span class="icon">📋</span> Extractions passees</h2>
|
||||
<div class="extraction-list" id="extractionList">
|
||||
{% if extractions %}
|
||||
{% for ext in extractions %}
|
||||
<div class="extraction-item">
|
||||
<div class="extraction-main">
|
||||
<div class="extraction-icon">📄</div>
|
||||
<div class="extraction-details">
|
||||
<h4>{{ ext.schema_name or ext.name or 'Extraction' }}</h4>
|
||||
<p>{{ ext.description or '' }} | {{ ext.date or '' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extraction-stats">
|
||||
<div class="extraction-stat">
|
||||
<div class="value">{{ ext.records_count or 0 }}</div>
|
||||
<div class="label">Enregistrements</div>
|
||||
</div>
|
||||
<div class="extraction-stat">
|
||||
<div class="value">{{ ext.fields_count or 0 }}</div>
|
||||
<div class="label">Champs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extraction-actions">
|
||||
<span class="status-badge {{ ext.status or 'completed' }}">{{ ext.status or 'completed' }}</span>
|
||||
{% if ext.id %}
|
||||
<button class="btn btn-success btn-small" onclick="exportCSV('{{ ext.id }}')">Export CSV</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">🔍</div>
|
||||
<h3>Aucune extraction</h3>
|
||||
<p>Les extractions apparaitront ici lorsque le moteur RPA aura extrait des donnees depuis les ecrans des applications.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function fetchJSON(url) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function refreshExtractions() {
|
||||
try {
|
||||
const data = await fetchJSON('/api/extractions');
|
||||
// Recharger la page pour afficher les nouvelles donnees
|
||||
// (plus simple que de mettre a jour le DOM Jinja)
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
console.error('Erreur refresh:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportCSV(extractionId) {
|
||||
try {
|
||||
const response = await fetch(`/api/extractions/${extractionId}/export?format=csv`);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `extraction_${extractionId}.csv`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
alert('Erreur export: ' + e.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
175
web_dashboard/templates/gestures.html
Normal file
175
web_dashboard/templates/gestures.html
Normal file
@@ -0,0 +1,175 @@
|
||||
<!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 - Gestes Primitifs</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; }
|
||||
.page-intro h2 { color: #e2e8f0; margin-bottom: 8px; font-size: 20px; }
|
||||
.page-intro p { color: #64748b; font-size: 14px; }
|
||||
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 20px; }
|
||||
.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; }
|
||||
|
||||
.category-section { margin-bottom: 30px; }
|
||||
.category-title { font-size: 18px; color: #e2e8f0; margin-bottom: 15px; display: flex; align-items: center; gap: 10px; padding-bottom: 10px; border-bottom: 1px solid #334155; }
|
||||
.category-title .cat-icon { font-size: 24px; }
|
||||
.category-title .count { font-size: 12px; color: #64748b; background: #334155; padding: 3px 10px; border-radius: 12px; margin-left: 10px; }
|
||||
|
||||
.gesture-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 15px; }
|
||||
.gesture-card { background: #0f172a; border: 1px solid #334155; border-radius: 10px; padding: 18px; transition: all 0.2s; cursor: default; }
|
||||
.gesture-card:hover { border-color: #3b82f6; transform: translateY(-2px); box-shadow: 0 4px 15px rgba(59, 130, 246, 0.1); }
|
||||
.gesture-card .gesture-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }
|
||||
.gesture-card .gesture-name { font-size: 15px; font-weight: 600; color: #e2e8f0; }
|
||||
.gesture-card .gesture-shortcut { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.gesture-card .gesture-shortcut kbd { background: #334155; color: #94a3b8; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-family: 'Segoe UI', monospace; border: 1px solid #475569; }
|
||||
.gesture-card .gesture-description { color: #64748b; font-size: 13px; line-height: 1.5; }
|
||||
.gesture-card .gesture-icon { font-size: 28px; margin-bottom: 8px; }
|
||||
|
||||
.unavailable-msg { text-align: center; padding: 60px 20px; color: #64748b; }
|
||||
.unavailable-msg .msg-icon { font-size: 48px; margin-bottom: 15px; }
|
||||
.unavailable-msg h3 { color: #94a3b8; margin-bottom: 10px; }
|
||||
.unavailable-msg p { font-size: 14px; max-width: 500px; margin: 0 auto; }
|
||||
|
||||
.filter-bar { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; }
|
||||
.filter-bar input { padding: 10px 15px; background: #0f172a; border: 1px solid #334155; border-radius: 8px; color: #e2e8f0; font-size: 14px; min-width: 300px; }
|
||||
.filter-bar input:focus { outline: none; border-color: #3b82f6; }
|
||||
.filter-bar .filter-count { color: #64748b; font-size: 13px; margin-left: auto; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-4 { grid-template-columns: 1fr 1fr; }
|
||||
.gesture-grid { grid-template-columns: 1fr; }
|
||||
.header { flex-direction: column; gap: 15px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>RPA Vision V3 - Gestes Primitifs</h1>
|
||||
<div class="nav-links">
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/gestures" class="active">Gestes</a>
|
||||
<a href="/streaming">Streaming</a>
|
||||
<a href="/extractions">Extractions</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="page-intro">
|
||||
<h2>Catalogue des gestes primitifs</h2>
|
||||
<p>Liste de tous les gestes (raccourcis clavier, actions systeme) connus par le moteur RPA. Ces gestes sont utilisables dans les workflows pour interagir avec les applications.</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-4">
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value" id="statTotal">{{ stats.total }}</div>
|
||||
<div class="stat-label">Total gestes</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value" id="statCategories">{{ stats.categories }}</div>
|
||||
<div class="stat-label">Categories</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value" id="statWithShortcut">{{ stats.with_shortcut }}</div>
|
||||
<div class="stat-label">Avec raccourci</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value">{{ stats.source }}</div>
|
||||
<div class="stat-label">Source</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not available %}
|
||||
<div class="card">
|
||||
<div class="unavailable-msg">
|
||||
<div class="msg-icon">⚠️</div>
|
||||
<h3>Module non disponible</h3>
|
||||
<p>Le module <code>agent_chat.gesture_catalog</code> n'est pas installe ou accessible. Les gestes ci-dessous sont le catalogue integre par defaut.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Filtre -->
|
||||
<div class="filter-bar">
|
||||
<input type="text" id="searchGestures" placeholder="Rechercher un geste (nom, raccourci, description)..." oninput="filterGestures()">
|
||||
<span class="filter-count" id="filterCount">{{ stats.total }} geste(s) affiche(s)</span>
|
||||
</div>
|
||||
|
||||
<!-- Gestes par categorie -->
|
||||
<div id="gesturesContainer">
|
||||
{% for category in categories %}
|
||||
<div class="category-section" data-category="{{ category.id }}">
|
||||
<div class="category-title">
|
||||
<span class="cat-icon">{{ category.icon }}</span>
|
||||
{{ category.name }}
|
||||
<span class="count">{{ category.gestures | length }}</span>
|
||||
</div>
|
||||
<div class="gesture-grid">
|
||||
{% for gesture in category.gestures %}
|
||||
<div class="gesture-card" data-search="{{ gesture.name|lower }} {{ gesture.description|lower }} {{ gesture.shortcut|lower if gesture.shortcut else '' }}">
|
||||
<div class="gesture-icon">{{ gesture.icon }}</div>
|
||||
<div class="gesture-header">
|
||||
<span class="gesture-name">{{ gesture.name }}</span>
|
||||
{% if gesture.shortcut %}
|
||||
<span class="gesture-shortcut">
|
||||
{% for key in gesture.shortcut.split('+') %}
|
||||
<kbd>{{ key }}</kbd>{% if not loop.last %}<span style="color:#475569;">+</span>{% endif %}
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="gesture-description">{{ gesture.description }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function filterGestures() {
|
||||
const query = document.getElementById('searchGestures').value.toLowerCase().trim();
|
||||
const cards = document.querySelectorAll('.gesture-card');
|
||||
const sections = document.querySelectorAll('.category-section');
|
||||
let visible = 0;
|
||||
|
||||
cards.forEach(card => {
|
||||
const searchText = card.getAttribute('data-search') || '';
|
||||
const match = !query || searchText.includes(query);
|
||||
card.style.display = match ? '' : 'none';
|
||||
if (match) visible++;
|
||||
});
|
||||
|
||||
// Masquer les categories vides
|
||||
sections.forEach(section => {
|
||||
const visibleCards = section.querySelectorAll('.gesture-card:not([style*="display: none"])');
|
||||
section.style.display = visibleCards.length > 0 ? '' : 'none';
|
||||
});
|
||||
|
||||
document.getElementById('filterCount').textContent = `${visible} geste(s) affiche(s)`;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -42,9 +42,16 @@
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🚀 RPA Vision V3 Dashboard</h1>
|
||||
<div class="status">
|
||||
<div class="status-dot" id="statusDot"></div>
|
||||
<span id="statusText">Connecté</span>
|
||||
<div style="display:flex;align-items:center;gap:20px;">
|
||||
<nav style="display:flex;gap:8px;">
|
||||
<a href="/gestures" style="color:rgba(255,255,255,0.8);text-decoration:none;font-size:13px;padding:6px 14px;border-radius:6px;transition:all 0.2s;background:rgba(255,255,255,0.1);" onmouseover="this.style.background='rgba(255,255,255,0.2)'" onmouseout="this.style.background='rgba(255,255,255,0.1)'">🤲 Gestes</a>
|
||||
<a href="/streaming" style="color:rgba(255,255,255,0.8);text-decoration:none;font-size:13px;padding:6px 14px;border-radius:6px;transition:all 0.2s;background:rgba(255,255,255,0.1);" onmouseover="this.style.background='rgba(255,255,255,0.2)'" onmouseout="this.style.background='rgba(255,255,255,0.1)'">📡 Streaming</a>
|
||||
<a href="/extractions" style="color:rgba(255,255,255,0.8);text-decoration:none;font-size:13px;padding:6px 14px;border-radius:6px;transition:all 0.2s;background:rgba(255,255,255,0.1);" onmouseover="this.style.background='rgba(255,255,255,0.2)'" onmouseout="this.style.background='rgba(255,255,255,0.1)'">📊 Extractions</a>
|
||||
</nav>
|
||||
<div class="status">
|
||||
<div class="status-dot" id="statusDot"></div>
|
||||
<span id="statusText">Connecté</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
360
web_dashboard/templates/streaming.html
Normal file
360
web_dashboard/templates/streaming.html
Normal file
@@ -0,0 +1,360 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user