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:
@@ -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">🔌</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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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...");
|
||||
|
||||
Reference in New Issue
Block a user