# 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() ) ))