Этап 5: окно настроек и сборка exe
- Окно настроек: модель и провайдер (со списком моделей с сервера), озвучка,
голосовой ввод, слово активации, автозапуск с Windows
- Сохранение в config.yaml через ruamel.yaml с комментариями и ссылками ${VAR},
проверка значений до записи; переключатели трея тоже сохраняются
- Настройки LLM и переключатели применяются на лету, для остального — перезапуск
- Один экземпляр приложения, лог в файл, аргументы --config и --wait-pid, русские диалоги Qt
- Сборка PyInstaller (onedir) с библиотеками CUDA, иконка приложения
- Тесты сохранения настроек и окна настроек
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a719980d74
commit
20a666e0d4
@@ -2,11 +2,16 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from PySide6.QtGui import QAction, QActionGroup
|
||||
from PySide6.QtWidgets import QApplication, QMenu, QSystemTrayIcon
|
||||
|
||||
from agr_assistent import APP_NAME
|
||||
from agr_assistent.config import ConfigError
|
||||
from agr_assistent.core.assistant import Assistant, AssistantState
|
||||
from agr_assistent.core.settings import Settings
|
||||
from agr_assistent.ui.chat_window import ChatWindow
|
||||
from agr_assistent.ui.icons import STATE_TITLES, state_icon
|
||||
|
||||
@@ -19,13 +24,14 @@ class TrayIcon(QSystemTrayIcon):
|
||||
self,
|
||||
assistant: Assistant,
|
||||
window: ChatWindow,
|
||||
settings: Settings,
|
||||
open_settings: Callable[[], None],
|
||||
hotkey: str = "",
|
||||
wake_phrases: list[str] | None = None,
|
||||
) -> None:
|
||||
wake_phrases = wake_phrases or []
|
||||
super().__init__(state_icon(assistant.state))
|
||||
self._assistant = assistant
|
||||
self._window = window
|
||||
self._settings = settings
|
||||
|
||||
menu = QMenu()
|
||||
menu.addAction("Открыть чат", window.show_and_raise)
|
||||
@@ -34,10 +40,11 @@ class TrayIcon(QSystemTrayIcon):
|
||||
label = f"Говорить\t{hotkey.title()}" if hotkey else "Говорить"
|
||||
menu.addAction(label, assistant.toggle_listening)
|
||||
if assistant.wake_word_available:
|
||||
phrases = ", ".join(f"«{phrase}»" for phrase in wake_phrases)
|
||||
phrases = ", ".join(f"«{phrase}»" for phrase in settings.config.wake_word.phrases)
|
||||
wake_action = QAction(f"Слушать слово активации: {phrases}", self, checkable=True)
|
||||
wake_action.setChecked(assistant.wake_word_enabled)
|
||||
wake_action.toggled.connect(assistant.set_wake_word_enabled)
|
||||
# triggered — только действие пользователя: автоотключение после ошибки не сохраняем
|
||||
wake_action.triggered.connect(self._on_wake_word_triggered)
|
||||
assistant.wake_word_enabled_changed.connect(wake_action.setChecked)
|
||||
menu.addAction(wake_action)
|
||||
menu.addSeparator()
|
||||
@@ -48,17 +55,18 @@ class TrayIcon(QSystemTrayIcon):
|
||||
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))
|
||||
action.triggered.connect(lambda _checked=False, n=name: self._on_provider_triggered(n))
|
||||
provider_group.addAction(action)
|
||||
provider_menu.addAction(action)
|
||||
self._provider_actions[name] = action
|
||||
|
||||
self._speech_action = QAction("Озвучивать ответы", self, checkable=True)
|
||||
self._speech_action.setChecked(assistant.speech_enabled)
|
||||
self._speech_action.toggled.connect(assistant.set_speech_enabled)
|
||||
self._speech_action.triggered.connect(self._on_speech_triggered)
|
||||
menu.addAction(self._speech_action)
|
||||
|
||||
menu.addAction("Новый диалог", assistant.clear_history)
|
||||
menu.addAction("Настройки…", open_settings)
|
||||
menu.addSeparator()
|
||||
menu.addAction("Выход", QApplication.quit)
|
||||
# Меню нужно держать в атрибуте, иначе его соберёт сборщик мусора
|
||||
@@ -82,6 +90,24 @@ class TrayIcon(QSystemTrayIcon):
|
||||
):
|
||||
self._window.toggle_visibility()
|
||||
|
||||
def _on_provider_triggered(self, name: str) -> None:
|
||||
self._assistant.set_provider(name)
|
||||
self._persist({"llm.provider": name})
|
||||
|
||||
def _on_speech_triggered(self, enabled: bool) -> None:
|
||||
self._assistant.set_speech_enabled(enabled)
|
||||
self._persist({"tts.enabled": enabled})
|
||||
|
||||
def _on_wake_word_triggered(self, enabled: bool) -> None:
|
||||
self._assistant.set_wake_word_enabled(enabled)
|
||||
self._persist({"wake_word.enabled": enabled})
|
||||
|
||||
def _persist(self, updates: dict[str, Any]) -> None:
|
||||
try:
|
||||
self._settings.save(updates)
|
||||
except (ConfigError, OSError) as exc:
|
||||
self._assistant.error_occurred.emit(f"Не удалось сохранить настройки: {exc}")
|
||||
|
||||
def _on_state_changed(self, state: AssistantState) -> None:
|
||||
self.setIcon(state_icon(state))
|
||||
self.setToolTip(f"{APP_NAME} — {STATE_TITLES[state]}")
|
||||
|
||||
Reference in New Issue
Block a user