perf: 1 appel VLM par screenshot + sélection intelligente + Rust auto-launch Léa

Analyse VLM :
- 1 seul appel VLM par screenshot au lieu de 30 (~15s vs 6.5min)
- Sélection screenshots par hash perceptuel (3-4 utiles sur 12)
- Fallback classification individuelle si appel unique échoue
- Estimation : ~1min par workflow au lieu de 78min

Rust agent :
- Léa (Edge mode app) s'ouvre automatiquement au démarrage
- Plus besoin de systray pour lancer le chat
- Fix URL chat /chat → /

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-03-19 00:26:29 +01:00
parent 90ee91caf9
commit 24a947b51d
6 changed files with 661 additions and 296 deletions

View File

@@ -1,277 +1,123 @@
//! Fenetre de chat WebView2 (wry).
//! Chat Léa via Edge en mode app (--app=URL).
//!
//! Ouvre une fenetre WebView2 qui charge l'interface de chat du serveur
//! (http://{server}:5004/chat). Plus simple et plus riche que l'approche
//! tkinter Python — on reutilise directement le frontend web existant.
//!
//! Equivalent de agent_v1/ui/chat_window.py (mais beaucoup plus simple).
//!
//! Sur Windows : utilise wry (crate Tauri) qui instancie Edge WebView2.
//! Sur les autres OS : pas de fenetre de chat (log en console).
//! Ouvre Edge sans barre d'adresse — rendu propre et professionnel.
//! Equivalent de agent_v1/ui/chat_window.py (approche Edge mode app).
use crate::config::Config;
use crate::state::AgentState;
use std::sync::Arc;
use std::process::Command;
/// URL du serveur de chat (port 5004 par defaut).
/// URL du serveur de chat
fn chat_url(config: &Config) -> String {
config.chat_url()
}
/// HTML de fallback affiche quand le serveur est indisponible.
#[allow(dead_code)]
const FALLBACK_HTML: &str = r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
font-family: 'Segoe UI', Tahoma, sans-serif;
background: #1e1e2e;
color: #cdd6f4;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
/// Chemin de Edge sur Windows (via le registre ou chemins courants)
fn find_edge() -> Option<String> {
let paths = [
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
];
for p in &paths {
if std::path::Path::new(p).exists() {
return Some(p.to_string());
}
.icon { font-size: 64px; margin-bottom: 20px; }
h2 { color: #89b4fa; margin-bottom: 10px; }
p { color: #a6adc8; text-align: center; max-width: 300px; line-height: 1.5; }
.retry-btn {
margin-top: 20px;
padding: 10px 24px;
background: #89b4fa;
color: #1e1e2e;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
// Essayer via le registre
#[cfg(target_os = "windows")]
{
use std::process::Command;
if let Ok(output) = Command::new("reg")
.args(&["query", r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe", "/ve"])
.output()
{
let text = String::from_utf8_lossy(&output.stdout);
for line in text.lines() {
if line.contains("REG_SZ") {
if let Some(path) = line.split("REG_SZ").last() {
let path = path.trim();
if std::path::Path::new(path).exists() {
return Some(path.to_string());
}
}
}
}
}
.retry-btn:hover { background: #74c7ec; }
</style>
</head>
<body>
<div class="icon">&#x1F50C;</div>
<h2>Connexion au serveur requise</h2>
<p>
Le serveur de chat n'est pas accessible.
Verifiez que le serveur RPA Vision est demarre.
</p>
<button class="retry-btn" onclick="location.reload()">Reessayer</button>
<p style="margin-top: 30px; font-size: 12px; color: #585b70;">
Lea Agent v0.2.0 (Rust) - IA
</p>
</body>
</html>"#;
/// Lance la fenetre de chat dans un thread dedie.
///
/// Sur Windows : ouvre un WebView2 qui charge l'URL du chat.
/// La fenetre peut etre masquee/affichee via l'etat partage.
/// Sur les autres OS : ne fait rien.
pub fn start_chat_thread(config: Arc<Config>, state: Arc<AgentState>) {
std::thread::Builder::new()
.name("chat-window".to_string())
.spawn(move || {
chat_window_loop(&config, &state);
})
.expect("Impossible de demarrer le thread chat");
}
None
}
/// Boucle de la fenetre de chat (Windows).
/// Lance le chat dans un thread.
///
/// Attend que l'etat chat_visible passe a true, puis ouvre la fenetre.
/// Quand la fenetre est fermee, remet chat_visible a false.
#[cfg(windows)]
fn chat_window_loop(config: &Config, state: &AgentState) {
println!("[CHAT] Thread chat demarre — en attente d'activation");
/// Attend que `state.chat_visible` passe à true, puis ouvre Edge en mode app.
/// Quand la fenêtre est fermée, remet `chat_visible` à false.
pub fn run_chat_thread(config: &Config, state: Arc<AgentState>) {
let url = chat_url(config);
let edge_path = find_edge();
if let Some(ref path) = edge_path {
println!("[CHAT] Edge trouvé : {}", path);
} else {
println!("[CHAT] Edge non trouvé — fallback navigateur par défaut");
}
loop {
// Attendre que le chat soit demande
while !state.chat_visible.load(std::sync::atomic::Ordering::SeqCst) {
// Attendre l'activation
while !state.chat_visible.load(std::sync::atomic::Ordering::Relaxed) {
if !state.is_running() {
println!("[CHAT] Arret du thread chat");
println!("[CHAT] Arrêt du thread chat");
return;
}
std::thread::sleep(std::time::Duration::from_millis(200));
}
println!("[CHAT] Ouverture de la fenetre de chat...");
let url = chat_url(config);
println!("[CHAT] Ouverture du chat...");
println!("[CHAT] URL : {}", url);
// Tester si le serveur est accessible
let server_available = reqwest::blocking::Client::new()
.get(&url)
.timeout(std::time::Duration::from_secs(3))
.send()
.map(|r| r.status().is_success() || r.status().is_redirection())
.unwrap_or(false);
let result = if let Some(ref path) = edge_path {
// Edge en mode app — fenêtre propre sans barre d'adresse
Command::new(path)
.args(&[
&format!("--app={}", url),
"--window-size=600,800",
"--window-position=1300,200",
"--disable-extensions",
"--no-first-run",
])
.spawn()
} else {
// Fallback : ouvrir dans le navigateur par défaut
#[cfg(target_os = "windows")]
{
Command::new("cmd")
.args(&["/C", "start", &url])
.spawn()
}
#[cfg(not(target_os = "windows"))]
{
Command::new("xdg-open")
.arg(&url)
.spawn()
}
};
// Ouvrir le WebView2 dans une fenetre dediee
// On utilise un EventLoop winit separe pour la fenetre de chat
match open_chat_window(&url, server_available) {
Ok(_) => {
println!("[CHAT] Fenetre de chat fermee");
match result {
Ok(mut child) => {
println!("[CHAT] Fenêtre ouverte (PID: {:?})", child.id());
// Attendre que la fenêtre se ferme
let _ = child.wait();
println!("[CHAT] Fenêtre fermée");
}
Err(e) => {
eprintln!("[CHAT] Erreur ouverture fenetre : {}", e);
println!("[CHAT] Erreur ouverture : {}", e);
}
}
// La fenetre a ete fermee, desactiver le flag
state
.chat_visible
.store(false, std::sync::atomic::Ordering::SeqCst);
// Marquer comme invisible
state.chat_visible.store(false, std::sync::atomic::Ordering::Relaxed);
// Petit delai avant de pouvoir reouvrir
// Petit délai avant de pouvoir réouvrir
std::thread::sleep(std::time::Duration::from_millis(500));
}
}
/// Ouvre la fenetre de chat avec wry WebView2.
///
/// Cree une fenetre native via la Win32 API et y attache un WebView2.
/// La fenetre fait 520x720 et est positionnee en bas a droite de l'ecran.
///
/// Note: wry 0.48 attend un objet implementant HasWindowHandle.
/// On utilise un wrapper HWND pour satisfaire ce trait.
#[cfg(windows)]
fn open_chat_window(url: &str, server_available: bool) -> Result<(), String> {
use wry::WebViewBuilder;
use raw_window_handle::{RawWindowHandle, WindowHandle, Win32WindowHandle};
use windows_sys::Win32::UI::WindowsAndMessaging::*;
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
// Obtenir les dimensions de l'ecran
let (screen_w, screen_h) = unsafe {
(GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN))
};
let win_w = 520;
let win_h = 720;
let win_x = screen_w - win_w - 20;
let win_y = screen_h - win_h - 60;
// Creer la classe de fenetre
let class_name: Vec<u16> = "LeaChatWindow\0".encode_utf16().collect();
let window_title: Vec<u16> = "Lea - Chat IA\0".encode_utf16().collect();
unsafe {
let h_instance = GetModuleHandleW(std::ptr::null());
let wc = WNDCLASSW {
style: 0,
lpfnWndProc: Some(chat_wnd_proc),
cbClsExtra: 0,
cbWndExtra: 0,
hInstance: h_instance,
hIcon: std::ptr::null_mut(),
hCursor: LoadCursorW(std::ptr::null_mut(), IDC_ARROW),
hbrBackground: 6 as _, // COLOR_WINDOW + 1
lpszMenuName: std::ptr::null(),
lpszClassName: class_name.as_ptr(),
};
RegisterClassW(&wc);
let hwnd = CreateWindowExW(
WS_EX_TOOLWINDOW,
class_name.as_ptr(),
window_title.as_ptr(),
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
win_x,
win_y,
win_w,
win_h,
std::ptr::null_mut(),
std::ptr::null_mut(),
h_instance,
std::ptr::null(),
);
if hwnd.is_null() {
return Err("Impossible de creer la fenetre de chat".to_string());
}
// Creer un wrapper HasWindowHandle pour le HWND
let mut win32_handle = Win32WindowHandle::new(
std::num::NonZero::new(hwnd as isize)
.ok_or("HWND invalide")?,
);
win32_handle.hinstance = std::num::NonZero::new(h_instance as isize);
let raw_handle = RawWindowHandle::Win32(win32_handle);
// SAFETY: le hwnd est valide pendant toute la duree de cette fonction
let window_handle = WindowHandle::borrow_raw(raw_handle);
// Creer le WebView2 dans la fenetre
let webview_result = if server_available {
WebViewBuilder::new()
.with_url(url)
.build_as_child(&window_handle)
} else {
WebViewBuilder::new()
.with_html(FALLBACK_HTML)
.build_as_child(&window_handle)
};
match webview_result {
Ok(_webview) => {
ShowWindow(hwnd, SW_SHOW);
// Boucle de messages Windows
let mut msg: MSG = std::mem::zeroed();
while GetMessageW(&mut msg, std::ptr::null_mut(), 0, 0) > 0 {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
Ok(())
}
Err(e) => {
DestroyWindow(hwnd);
Err(format!("Erreur creation WebView2 : {}", e))
}
}
}
}
/// Procedure de fenetre Win32 pour la fenetre de chat.
#[cfg(windows)]
unsafe extern "system" fn chat_wnd_proc(
hwnd: windows_sys::Win32::Foundation::HWND,
msg: u32,
wparam: windows_sys::Win32::Foundation::WPARAM,
lparam: windows_sys::Win32::Foundation::LPARAM,
) -> windows_sys::Win32::Foundation::LRESULT {
use windows_sys::Win32::UI::WindowsAndMessaging::*;
match msg {
WM_CLOSE => {
ShowWindow(hwnd, SW_HIDE);
PostQuitMessage(0);
0
}
WM_DESTROY => {
PostQuitMessage(0);
0
}
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
/// Version non-Windows : pas de fenetre de chat.
#[cfg(not(windows))]
fn chat_window_loop(config: &Config, state: &AgentState) {
println!("[CHAT] Fenetre de chat non disponible sur cet OS");
let url = chat_url(config);
println!("[CHAT] Pour acceder au chat, ouvrez : {}", url);
while state.is_running() {
std::thread::sleep(std::time::Duration::from_millis(1000));
}
}

View File

@@ -135,13 +135,13 @@ impl Config {
if let Some(colon_pos) = after_scheme.find(':') {
let host = &after_scheme[..colon_pos];
return format!(
"http://{}:{}/chat?machine_id={}",
"http://{}:{}/?machine_id={}",
host, self.chat_port, self.machine_id
);
}
}
format!(
"http://localhost:{}/chat?machine_id={}",
"http://localhost:{}/?machine_id={}",
self.chat_port, self.machine_id
)
}

View File

@@ -40,6 +40,21 @@ use std::sync::Arc;
use std::thread;
use std::time::Duration;
/// Trouve Edge sur Windows
#[cfg(target_os = "windows")]
fn find_edge() -> Option<String> {
let paths = [
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
];
for p in &paths {
if std::path::Path::new(p).exists() {
return Some(p.to_string());
}
}
None
}
fn main() {
// Initialiser le logging
env_logger::Builder::from_env(
@@ -118,13 +133,35 @@ fn main() {
// Thread 6 : Chat window (WebView2, a la demande)
let chat_config = config.clone();
let chat_state = state.clone();
chat::start_chat_thread(chat_config, chat_state);
chat::run_chat_thread(&chat_config, chat_state);
println!("\n[MAIN] Agent operationnel — tous les threads demarres.\n");
// Thread principal : boucle systray (Windows) ou attente console (Linux)
// Le systray bloque le thread principal (necessaire pour la message pump Windows)
tray::run_tray_loop(config.clone(), state.clone());
// Ouvrir Léa (Edge mode app) automatiquement au démarrage
#[cfg(target_os = "windows")]
{
let chat_url = config.chat_url();
if let Some(edge) = find_edge() {
println!("[MAIN] Ouverture de Léa dans Edge...");
let _ = std::process::Command::new(&edge)
.args(&[
&format!("--app={}", chat_url),
"--window-size=600,800",
"--disable-extensions",
"--no-first-run",
])
.spawn();
}
}
// Attente principale : Ctrl+C pour arrêter
println!("[MAIN] Appuyez sur Ctrl+C pour quitter.\n");
loop {
if !state.is_running() {
break;
}
thread::sleep(Duration::from_millis(500));
}
// Si on arrive ici, l'agent doit s'arreter
println!("\n[MAIN] Arret en cours...");