from PySide6.QtCore import QEasingCurve, QPoint, QPropertyAnimation, Qt, Signal from PySide6.QtWidgets import ( QApplication, QLabel, QLineEdit, QMainWindow, QVBoxLayout, QWidget, ) class RisaAssistantPopup(QMainWindow): text_submitted = Signal(str) def __init__(self): super().__init__() self.setWindowFlags( Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Tool ) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.popup_width = 540 self.font_style = "font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Arial, sans-serif;" self.main_container = QWidget(self) self.main_layout = QVBoxLayout(self.main_container) self.main_layout.setContentsMargins(10, 10, 10, 10) self.main_layout.setSpacing(15) self.content_card = QWidget() self.content_card.setObjectName("ContentCard") self.content_layout = QVBoxLayout(self.content_card) self.content_layout.setContentsMargins(22, 16, 22, 16) self.content_label = QLabel() self.content_label.setTextFormat(Qt.TextFormat.RichText) self.content_label.setWordWrap(True) self.content_layout.addWidget(self.content_label) self.main_layout.addWidget(self.content_card) self.input_card = QWidget() self.input_card.setObjectName("InputCard") self.input_layout = QVBoxLayout(self.input_card) self.input_layout.setContentsMargins(20, 20, 20, 20) self.input_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) self.star_icon = QLabel("✹") self.star_icon.setStyleSheet("font-size: 42px; color: #000000; margin-bottom: 2px;") self.star_icon.setAlignment(Qt.AlignmentFlag.AlignCenter) self.input_layout.addWidget(self.star_icon) self.status_label = QLabel("Sleeping...") self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.status_label.setStyleSheet("font-size: 13px; color: #777777; font-weight: 600; margin-bottom: 5px;") self.input_layout.addWidget(self.status_label) self.input_field = QLineEdit() self.input_field.setPlaceholderText("Type or Speak to RISA...") self.input_field.setAlignment(Qt.AlignmentFlag.AlignCenter) self.input_field.setFrame(False) self.input_layout.addWidget(self.input_field) self.input_field.returnPressed.connect(self._on_text_submitted) self.main_layout.addWidget(self.input_card) self.setCentralWidget(self.main_container) self.setStyleSheet(""" QWidget#ContentCard, QWidget#InputCard { background-color: rgba(245, 245, 240, 0.78); border: 1px solid rgba(255, 255, 255, 0.45); border-radius: 28px; } QLineEdit { background: transparent; color: #000000; font-family: 'Segoe UI', Arial, sans-serif; font-size: 18px; } QLineEdit::placeholder { color: #555555; } """) self.content_card.hide() self.anim = None def _on_text_submitted(self): text = self.input_field.text().strip() if text: self.input_field.clear() self.text_submitted.emit(text) def set_listening_state(self, state: str): if state == "idle": self.star_icon.setStyleSheet("font-size: 42px; color: #888888; margin-bottom: 2px;") self.status_label.setText("Sleeping (Say 'Hey Risa')...") self.status_label.setStyleSheet("font-size: 13px; color: #888888; font-weight: 600; margin-bottom: 5px;") elif state == "active": self.star_icon.setStyleSheet("font-size: 42px; color: #D4FF00; margin-bottom: 2px;") self.status_label.setText("Listening...") self.status_label.setStyleSheet("font-size: 13px; color: #ff8c00; font-weight: bold; margin-bottom: 5px;") elif state == "thinking": self.star_icon.setStyleSheet("font-size: 42px; color: #007acc; margin-bottom: 2px;") self.status_label.setText("Thinking...") self.status_label.setStyleSheet("font-size: 13px; color: #007acc; font-weight: bold; margin-bottom: 5px;") def update_layout_and_animate(self): self.setFixedWidth(self.popup_width) self.adjustSize() self.resize(self.popup_width, self.sizeHint().height()) self.animate_popup(self.height()) def show_initial_input(self): self.content_card.hide() self.input_layout.setContentsMargins(20, 25, 20, 25) self.star_icon.show() self.status_label.show() self.input_field.setFocus() self.update_layout_and_animate() def show_thinking(self, prompt_text: str): html_content = f"""
Risa • Asking AI...
"{prompt_text}"
✹ Thinking...
""" self.content_label.setText(html_content) self.content_card.show() self.input_layout.setContentsMargins(20, 20, 20, 20) self.update_layout_and_animate() def display_command_card(self, mode: str, data: dict = None): if data is None: data = {} html_content = "" if mode == "weather": html_content = f"""
{data.get('temp', '72')}° {data.get('humidity', '55')}% Humidity
{data.get('high', '85')}° High    {data.get('low', '63')}° Low
{data.get('condition', 'Sunny')} {data.get('location', 'Bridgewater NJ')}
""" elif mode == "time": html_content = f"""
{data.get('time', '3:30 PM')} {data.get('day', '')}
Sunny, rain soon. {data.get('date', '')}
""" elif mode == "general": html_content = f"""
Risa Assistant
{data.get('text', '')}
""" if html_content: self.content_label.setText(html_content) self.content_card.show() self.input_layout.setContentsMargins(20, 20, 20, 20) self.update_layout_and_animate() def animate_popup(self, current_height): screen = QApplication.primaryScreen() screen_geom = screen.availableGeometry() self.end_x = screen_geom.left() + int((screen_geom.width() - self.popup_width) / 2) self.end_y = screen_geom.bottom() - current_height - 25 self.start_y = screen_geom.bottom() + 50 if self.anim and self.anim.state() == QPropertyAnimation.State.Running: self.anim.stop() if not self.isVisible(): self.move(self.end_x, self.start_y) self.show() self.anim = QPropertyAnimation(self, b"pos") self.anim.setDuration(350) self.anim.setStartValue(self.pos()) self.anim.setEndValue(QPoint(self.end_x, self.end_y)) self.anim.setEasingCurve(QEasingCurve.Type.OutCubic) self.anim.start() def changeEvent(self, event): if event.type() == event.Type.ActivationChange and not self.isActiveWindow(): self.hide() super().changeEvent(event)