211 lines
9.1 KiB
Python
211 lines
9.1 KiB
Python
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"""
|
|
<div style="{self.font_style} color: #111111; font-size: 16px; font-weight: 500; line-height: 140%;">
|
|
<span style="color: #000000; font-size: 12px; font-weight: bold; text-transform: uppercase; letter-spacing: 1px;">Risa • Asking AI...</span><br>
|
|
<div style="padding-top: 6px; color: #555555; font-style: italic;">"{prompt_text}"</div>
|
|
<div style="padding-top: 10px; font-size: 14px; color: #ff8c00; font-weight: bold;">✹ Thinking...</div>
|
|
</div>
|
|
"""
|
|
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"""
|
|
<table width='100%' style="{self.font_style}">
|
|
<tr>
|
|
<td style='font-size: 56px; font-weight: bold; color: #111111; line-height: 100%;'>{data.get('temp', '72')}°</td>
|
|
<td align='right' style='font-size: 14px; color: #444444; font-weight: 500;'>{data.get('humidity', '55')}% Humidity<br>
|
|
<span style='color:#777777; font-size: 13px;'>{data.get('high', '85')}° High {data.get('low', '63')}° Low</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td style='font-size: 15px; color: #222222; font-weight: 600; padding-top: 6px;'>{data.get('condition', 'Sunny')}</td>
|
|
<td align='right' style='font-size: 15px; color: #222222; font-weight: 600; padding-top: 6px;'>{data.get('location', 'Bridgewater NJ')}</td>
|
|
</tr>
|
|
</table>
|
|
"""
|
|
elif mode == "time":
|
|
html_content = f"""
|
|
<table width='100%' style="{self.font_style}">
|
|
<tr>
|
|
<td style='font-size: 42px; font-weight: 800; color: #111111; line-height: 100%;'>{data.get('time', '3:30 PM')}</td>
|
|
<td align='right' style='font-size: 15px; color: #111111; font-weight: 600;'>{data.get('day', '')}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style='font-size: 15px; color: #222222; font-weight: 600; padding-top: 4px;'>Sunny, rain soon.</td>
|
|
<td align='right' style='font-size: 14px; color: #777777; font-weight: 500; padding-top: 4px;'>{data.get('date', '')}</td>
|
|
</tr>
|
|
</table>
|
|
"""
|
|
elif mode == "general":
|
|
html_content = f"""
|
|
<div style="{self.font_style} color: #111111; font-size: 16px; font-weight: 500; line-height: 145%;">
|
|
<span style="color: #666666; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 1px;">Risa Assistant</span><br>
|
|
<div style="padding-top: 6px;">{data.get('text', '')}</div>
|
|
</div>
|
|
"""
|
|
|
|
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) |