Этап 1: трей, окно чата и текстовый диалог с LLM
- PySide6: значок в трее с индикацией состояния, окно чата со стримингом и markdown
- Единый OpenAI-совместимый клиент (Ollama, LM Studio, OpenRouter)
- config.yaml с автосозданием, слиянием с умолчаниями и подстановкой ${ENV}
- Тесты конфига и сборки сообщений
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
26a01aa210
commit
7933613234
@@ -0,0 +1,81 @@
|
||||
"""Значок в системном трее и его меню."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtGui import QAction, QActionGroup
|
||||
from PySide6.QtWidgets import QApplication, QMenu, QSystemTrayIcon
|
||||
|
||||
from agr_assistent import APP_NAME
|
||||
from agr_assistent.core.assistant import Assistant, AssistantState
|
||||
from agr_assistent.ui.chat_window import ChatWindow
|
||||
from agr_assistent.ui.icons import STATE_TITLES, state_icon
|
||||
|
||||
_NOTIFICATION_PREVIEW_CHARS = 200
|
||||
_NOTIFICATION_TIMEOUT_MS = 6000
|
||||
|
||||
|
||||
class TrayIcon(QSystemTrayIcon):
|
||||
def __init__(self, assistant: Assistant, window: ChatWindow) -> None:
|
||||
super().__init__(state_icon(assistant.state))
|
||||
self._assistant = assistant
|
||||
self._window = window
|
||||
|
||||
menu = QMenu()
|
||||
menu.addAction("Открыть чат", window.show_and_raise)
|
||||
|
||||
provider_menu = menu.addMenu("Провайдер LLM")
|
||||
provider_group = QActionGroup(self)
|
||||
self._provider_actions: dict[str, QAction] = {}
|
||||
for name in assistant.provider_names:
|
||||
action = QAction(name, self, checkable=True)
|
||||
action.setChecked(name == assistant.provider_name)
|
||||
action.triggered.connect(lambda _checked=False, n=name: assistant.set_provider(n))
|
||||
provider_group.addAction(action)
|
||||
provider_menu.addAction(action)
|
||||
self._provider_actions[name] = action
|
||||
|
||||
menu.addAction("Новый диалог", assistant.clear_history)
|
||||
menu.addSeparator()
|
||||
menu.addAction("Выход", QApplication.quit)
|
||||
# Меню нужно держать в атрибуте, иначе его соберёт сборщик мусора
|
||||
self._menu = menu
|
||||
self.setContextMenu(menu)
|
||||
|
||||
self.activated.connect(self._on_activated)
|
||||
self.messageClicked.connect(window.show_and_raise)
|
||||
assistant.state_changed.connect(self._on_state_changed)
|
||||
assistant.provider_changed.connect(self._on_provider_changed)
|
||||
assistant.reply_finished.connect(self._on_reply_finished)
|
||||
assistant.error_occurred.connect(self._on_error)
|
||||
|
||||
self._on_state_changed(assistant.state)
|
||||
|
||||
def _on_activated(self, reason: QSystemTrayIcon.ActivationReason) -> None:
|
||||
if reason in (
|
||||
QSystemTrayIcon.ActivationReason.Trigger,
|
||||
QSystemTrayIcon.ActivationReason.DoubleClick,
|
||||
):
|
||||
self._window.toggle_visibility()
|
||||
|
||||
def _on_state_changed(self, state: AssistantState) -> None:
|
||||
self.setIcon(state_icon(state))
|
||||
self.setToolTip(f"{APP_NAME} — {STATE_TITLES[state]}")
|
||||
|
||||
def _on_provider_changed(self, name: str) -> None:
|
||||
if action := self._provider_actions.get(name):
|
||||
action.setChecked(True)
|
||||
|
||||
def _on_reply_finished(self, text: str) -> None:
|
||||
if text.strip() and not self._window.isVisible():
|
||||
preview = text.strip()
|
||||
if len(preview) > _NOTIFICATION_PREVIEW_CHARS:
|
||||
preview = preview[:_NOTIFICATION_PREVIEW_CHARS].rstrip() + "…"
|
||||
self.showMessage(
|
||||
APP_NAME, preview, QSystemTrayIcon.MessageIcon.Information, _NOTIFICATION_TIMEOUT_MS
|
||||
)
|
||||
|
||||
def _on_error(self, message: str) -> None:
|
||||
if not self._window.isVisible():
|
||||
self.showMessage(
|
||||
APP_NAME, message, QSystemTrayIcon.MessageIcon.Warning, _NOTIFICATION_TIMEOUT_MS
|
||||
)
|
||||
Reference in New Issue
Block a user