Files
rpa_vision_v3/agent_v0/lea_ui/chat_widget.py
Dom ae65be2555 chore: ajouter agent_v0/ au tracking git (était un repo embarqué)
Suppression du .git embarqué dans agent_v0/ — le code est maintenant
tracké normalement dans le repo principal.
Inclut : agent_v1 (client), server_v1 (streaming), lea_ui (chat client)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:12:23 +01:00

251 lines
7.9 KiB
Python

# agent_v0/lea_ui/chat_widget.py
"""
Widget de chat pour l'interface Lea.
Affiche les messages avec des bulles :
- Utilisateur a droite (fond indigo)
- Lea a gauche (fond blanc)
Communique avec le serveur Linux via LeaServerClient.
"""
from __future__ import annotations
import logging
from typing import List, Optional
from PyQt5.QtCore import (
QPropertyAnimation,
QSize,
Qt,
QTimer,
pyqtSignal,
pyqtSlot,
)
from PyQt5.QtGui import QColor, QFont, QPainter, QPainterPath, QPen
from PyQt5.QtWidgets import (
QFrame,
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QScrollArea,
QSizePolicy,
QVBoxLayout,
QWidget,
)
from . import styles
logger = logging.getLogger("lea_ui.chat")
class ChatBubble(QFrame):
"""Bulle de message individuelle."""
def __init__(
self,
text: str,
is_user: bool = False,
parent: Optional[QWidget] = None,
) -> None:
super().__init__(parent)
self._is_user = is_user
# Style de la bulle
if is_user:
bg_color = styles.COLOR_BUBBLE_USER
text_color = styles.COLOR_TEXT_ON_ACCENT
align = Qt.AlignRight
else:
bg_color = styles.COLOR_BUBBLE_LEA
text_color = styles.COLOR_TEXT
align = Qt.AlignLeft
self.setStyleSheet(f"""
QFrame {{
background-color: {bg_color};
border-radius: {styles.BUBBLE_RADIUS}px;
padding: {styles.PADDING}px;
border: {"none" if is_user else f"1px solid {styles.COLOR_BORDER}"};
}}
""")
layout = QVBoxLayout(self)
layout.setContentsMargins(
styles.PADDING, styles.PADDING // 2,
styles.PADDING, styles.PADDING // 2,
)
label = QLabel(text)
label.setWordWrap(True)
label.setFont(QFont(styles.FONT_FAMILY, styles.FONT_SIZE_NORMAL))
label.setStyleSheet(f"color: {text_color}; background: transparent; border: none;")
label.setTextFormat(Qt.RichText)
label.setOpenExternalLinks(True)
layout.addWidget(label)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
self.setMaximumWidth(280)
class ChatWidget(QWidget):
"""Widget de chat complet avec zone de messages et champ de saisie.
Signals :
message_sent(str) : emis quand l'utilisateur envoie un message
"""
message_sent = pyqtSignal(str)
def __init__(self, parent: Optional[QWidget] = None) -> None:
super().__init__(parent)
self._messages: List[dict] = []
self._setup_ui()
def _setup_ui(self) -> None:
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Zone de messages (scrollable)
self._scroll_area = QScrollArea()
self._scroll_area.setWidgetResizable(True)
self._scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self._scroll_area.setStyleSheet(styles.CHAT_AREA_STYLE)
self._messages_container = QWidget()
self._messages_container.setObjectName("ChatContainer")
self._messages_layout = QVBoxLayout(self._messages_container)
self._messages_layout.setContentsMargins(
styles.PADDING, styles.PADDING,
styles.PADDING, styles.PADDING,
)
self._messages_layout.setSpacing(styles.SPACING)
self._messages_layout.addStretch()
self._scroll_area.setWidget(self._messages_container)
layout.addWidget(self._scroll_area, stretch=1)
# Separateur
sep = QFrame()
sep.setFrameShape(QFrame.HLine)
sep.setStyleSheet(f"background-color: {styles.COLOR_BORDER}; max-height: 1px;")
layout.addWidget(sep)
# Zone de saisie
input_layout = QHBoxLayout()
input_layout.setContentsMargins(
styles.PADDING, styles.SPACING,
styles.PADDING, styles.SPACING,
)
input_layout.setSpacing(styles.SPACING)
self._input = QLineEdit()
self._input.setObjectName("ChatInput")
self._input.setPlaceholderText("Ecrivez un message...")
self._input.setStyleSheet(styles.INPUT_STYLE)
self._input.returnPressed.connect(self._on_send)
input_layout.addWidget(self._input, stretch=1)
self._send_btn = QPushButton("Envoyer")
self._send_btn.setObjectName("SendButton")
self._send_btn.setStyleSheet(styles.SEND_BUTTON_STYLE)
self._send_btn.setCursor(Qt.PointingHandCursor)
self._send_btn.clicked.connect(self._on_send)
input_layout.addWidget(self._send_btn)
layout.addLayout(input_layout)
def _on_send(self) -> None:
"""Envoyer le message saisi."""
text = self._input.text().strip()
if not text:
return
self._input.clear()
self.add_user_message(text)
self.message_sent.emit(text)
# ---------------------------------------------------------------------------
# API publique
# ---------------------------------------------------------------------------
def add_user_message(self, text: str) -> None:
"""Ajouter un message utilisateur (bulle a droite)."""
self._add_bubble(text, is_user=True)
def add_lea_message(self, text: str) -> None:
"""Ajouter un message de Lea (bulle a gauche)."""
self._add_bubble(text, is_user=False)
def add_system_message(self, text: str) -> None:
"""Ajouter un message systeme (centre, discret)."""
label = QLabel(text)
label.setFont(QFont(styles.FONT_FAMILY, styles.FONT_SIZE_SMALL))
label.setStyleSheet(
f"color: {styles.COLOR_TEXT_SECONDARY}; "
f"background: transparent; padding: 4px;"
)
label.setAlignment(Qt.AlignCenter)
label.setWordWrap(True)
# Inserer avant le stretch final
count = self._messages_layout.count()
self._messages_layout.insertWidget(count - 1, label)
self._scroll_to_bottom()
def set_input_enabled(self, enabled: bool) -> None:
"""Activer/desactiver la saisie (pendant le chargement)."""
self._input.setEnabled(enabled)
self._send_btn.setEnabled(enabled)
if not enabled:
self._input.setPlaceholderText("Lea reflechit...")
else:
self._input.setPlaceholderText("Ecrivez un message...")
def clear_messages(self) -> None:
"""Effacer tous les messages."""
while self._messages_layout.count() > 1:
item = self._messages_layout.takeAt(0)
widget = item.widget()
if widget:
widget.deleteLater()
self._messages = []
# ---------------------------------------------------------------------------
# Internals
# ---------------------------------------------------------------------------
def _add_bubble(self, text: str, is_user: bool) -> None:
"""Ajouter une bulle au conteneur de messages."""
bubble = ChatBubble(text, is_user=is_user)
# Conteneur d'alignement
row = QHBoxLayout()
row.setContentsMargins(0, 0, 0, 0)
if is_user:
row.addStretch()
row.addWidget(bubble)
else:
row.addWidget(bubble)
row.addStretch()
# Inserer avant le stretch final
count = self._messages_layout.count()
wrapper = QWidget()
wrapper.setLayout(row)
wrapper.setStyleSheet("background: transparent;")
self._messages_layout.insertWidget(count - 1, wrapper)
self._messages.append({"text": text, "is_user": is_user})
self._scroll_to_bottom()
def _scroll_to_bottom(self) -> None:
"""Scroller vers le bas apres l'ajout d'un message."""
QTimer.singleShot(50, lambda: (
self._scroll_area.verticalScrollBar().setValue(
self._scroll_area.verticalScrollBar().maximum()
)
))