Compare commits
3 Commits
cbd3d40e39
...
2b1743c206
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b1743c206 | ||
|
|
48879fb849 | ||
|
|
c12fd8e1c1 |
@@ -5,6 +5,9 @@ Fenetre de chat Lea integree au systray — version tkinter native.
|
|||||||
Remplace l'approche Edge browser par une vraie fenetre tkinter integree.
|
Remplace l'approche Edge browser par une vraie fenetre tkinter integree.
|
||||||
Design professionnel, theme clair, ancree en bas a droite de l'ecran.
|
Design professionnel, theme clair, ancree en bas a droite de l'ecran.
|
||||||
Tourne dans son propre thread daemon pour ne pas bloquer pystray.
|
Tourne dans son propre thread daemon pour ne pas bloquer pystray.
|
||||||
|
|
||||||
|
Le runtime Python embedded Windows ne contient pas toujours Tcl/Tk. Dans ce
|
||||||
|
cas, le menu "Discuter avec Lea" ouvre le chat DGX dans le navigateur.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -13,6 +16,8 @@ import math
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlparse
|
||||||
from typing import Any, Callable, Dict, Optional
|
from typing import Any, Callable, Dict, Optional
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -219,7 +224,10 @@ class ChatWindow:
|
|||||||
|
|
||||||
def toggle(self) -> None:
|
def toggle(self) -> None:
|
||||||
"""Afficher/masquer la fenetre de chat."""
|
"""Afficher/masquer la fenetre de chat."""
|
||||||
if self._destroyed or self._root is None:
|
if self._destroyed:
|
||||||
|
return
|
||||||
|
if self._root is None:
|
||||||
|
self._open_browser_fallback()
|
||||||
return
|
return
|
||||||
if self._visible:
|
if self._visible:
|
||||||
self.hide()
|
self.hide()
|
||||||
@@ -228,7 +236,10 @@ class ChatWindow:
|
|||||||
|
|
||||||
def show(self) -> None:
|
def show(self) -> None:
|
||||||
"""Afficher la fenetre."""
|
"""Afficher la fenetre."""
|
||||||
if self._destroyed or self._root is None:
|
if self._destroyed:
|
||||||
|
return
|
||||||
|
if self._root is None:
|
||||||
|
self._open_browser_fallback()
|
||||||
return
|
return
|
||||||
self._root.after(0, self._do_show)
|
self._root.after(0, self._do_show)
|
||||||
|
|
||||||
@@ -257,6 +268,79 @@ class ChatWindow:
|
|||||||
"""Mettre a jour le client serveur (appele si cree apres la fenetre)."""
|
"""Mettre a jour le client serveur (appele si cree apres la fenetre)."""
|
||||||
self._server_client = server_client
|
self._server_client = server_client
|
||||||
|
|
||||||
|
def _chat_url(self) -> str:
|
||||||
|
"""Retourne l'URL web du chat, derivee de la config serveur."""
|
||||||
|
configured_url = self._chat_url_from_server_url(self._configured_server_url())
|
||||||
|
if self._server_client is not None:
|
||||||
|
chat_base = getattr(self._server_client, "_chat_base", None)
|
||||||
|
if chat_base:
|
||||||
|
chat_base = str(chat_base).rstrip("/")
|
||||||
|
if not self._is_local_url(chat_base):
|
||||||
|
return chat_base
|
||||||
|
if configured_url:
|
||||||
|
return configured_url
|
||||||
|
|
||||||
|
if configured_url:
|
||||||
|
return configured_url
|
||||||
|
|
||||||
|
host = (self._server_host or "localhost").strip()
|
||||||
|
if host.startswith(("http://", "https://")):
|
||||||
|
parsed = urlparse(host)
|
||||||
|
scheme = parsed.scheme or "http"
|
||||||
|
hostname = parsed.hostname or "localhost"
|
||||||
|
return f"{scheme}://{hostname}:{self._chat_port}"
|
||||||
|
|
||||||
|
return f"http://{host}:{self._chat_port}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_local_url(url: str) -> bool:
|
||||||
|
try:
|
||||||
|
host = urlparse(url).hostname
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return host in {"localhost", "127.0.0.1", "::1"}
|
||||||
|
|
||||||
|
def _chat_url_from_server_url(self, server_url: Optional[str]) -> Optional[str]:
|
||||||
|
if not server_url:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
parsed = urlparse(server_url.strip())
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
if not parsed.hostname or parsed.hostname in {"localhost", "127.0.0.1", "::1"}:
|
||||||
|
return None
|
||||||
|
scheme = parsed.scheme or "http"
|
||||||
|
return f"{scheme}://{parsed.hostname}:{self._chat_port}"
|
||||||
|
|
||||||
|
def _configured_server_url(self) -> Optional[str]:
|
||||||
|
env_url = os.environ.get("RPA_SERVER_URL", "").strip()
|
||||||
|
if env_url:
|
||||||
|
return env_url
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Installed layout: <app>/agent_v1/ui/chat_window.py.
|
||||||
|
for parent in Path(__file__).resolve().parents:
|
||||||
|
cfg = parent / "config.txt"
|
||||||
|
if cfg.exists():
|
||||||
|
for line in cfg.read_text(encoding="utf-8", errors="ignore").splitlines():
|
||||||
|
if line.startswith("RPA_SERVER_URL="):
|
||||||
|
return line.split("=", 1)[1].strip()
|
||||||
|
except Exception:
|
||||||
|
logger.debug("Lecture config.txt pour chat_url impossible", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _open_browser_fallback(self) -> None:
|
||||||
|
"""Fallback POC quand tkinter est absent du Python embedded."""
|
||||||
|
url = self._chat_url()
|
||||||
|
try:
|
||||||
|
import webbrowser
|
||||||
|
if webbrowser.open(url, new=1):
|
||||||
|
logger.info("ChatWindow indisponible, chat ouvert dans le navigateur: %s", url)
|
||||||
|
else:
|
||||||
|
logger.warning("ChatWindow indisponible, ouverture navigateur refusee: %s", url)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Impossible d'ouvrir le chat dans le navigateur (%s): %s", url, exc)
|
||||||
|
|
||||||
def _on_shared_state_change(self, state) -> None:
|
def _on_shared_state_change(self, state) -> None:
|
||||||
"""Callback appele quand l'etat partage change (depuis le systray ou ailleurs).
|
"""Callback appele quand l'etat partage change (depuis le systray ou ailleurs).
|
||||||
|
|
||||||
|
|||||||
@@ -330,7 +330,8 @@ def import_learned_workflow(workflow_id: str):
|
|||||||
|
|
||||||
# Extraire et sauvegarder le screenshot d'ancre si présent
|
# Extraire et sauvegarder le screenshot d'ancre si présent
|
||||||
anchor_b64 = params.pop("_anchor_image_base64", None)
|
anchor_b64 = params.pop("_anchor_image_base64", None)
|
||||||
params.pop("_anchor_bbox", None)
|
# NE PAS supprimer _anchor_bbox : on le conserve dans params pour que le frontend puisse lire x_pct/y_pct
|
||||||
|
# et afficher la zone ciblée, au lieu de le jeter et de créer une bbox factice.
|
||||||
if anchor_b64:
|
if anchor_b64:
|
||||||
try:
|
try:
|
||||||
from services.anchor_image_service import (
|
from services.anchor_image_service import (
|
||||||
@@ -344,6 +345,8 @@ def import_learned_workflow(workflow_id: str):
|
|||||||
anchor_b64 = anchor_b64.split(',', 1)[1]
|
anchor_b64 = anchor_b64.split(',', 1)[1]
|
||||||
img_data = b64mod.b64decode(anchor_b64)
|
img_data = b64mod.b64decode(anchor_b64)
|
||||||
img = Image.open(BytesIO(img_data))
|
img = Image.open(BytesIO(img_data))
|
||||||
|
# Fallback sécurisé pour le service de crop si _anchor_bbox n'a pas le format attendu,
|
||||||
|
# mais les données x_pct/y_pct restent intactes dans params pour le frontend.
|
||||||
bbox = {
|
bbox = {
|
||||||
"x": 0, "y": 0,
|
"x": 0, "y": 0,
|
||||||
"width": img.width, "height": img.height
|
"width": img.width, "height": img.height
|
||||||
|
|||||||
@@ -2800,7 +2800,10 @@
|
|||||||
|
|
||||||
// Utiliser le proxy du dashboard pour éviter les problèmes CORS
|
// Utiliser le proxy du dashboard pour éviter les problèmes CORS
|
||||||
const STREAMING_BASE = '/api/streaming';
|
const STREAMING_BASE = '/api/streaming';
|
||||||
const VWB_IMPORT_URL = 'http://localhost:5002/api/workflows/import-core';
|
// Construire VWB_IMPORT_URL dynamiquement à partir de l'origine actuelle (ex: http://192.168.1.45:5001 -> http://192.168.1.45:5002)
|
||||||
|
// pour éviter le hardcoded localhost et permettre les tests depuis la VM/poste via l'IP du banc.
|
||||||
|
const VWB_BASE = window.location.origin.replace(/:\d+$/, ':5002');
|
||||||
|
const VWB_IMPORT_URL = `${VWB_BASE}/api/workflows/import-core`;
|
||||||
|
|
||||||
async function refreshStreaming() {
|
async function refreshStreaming() {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|||||||
Reference in New Issue
Block a user