J.A.R.V.I.S-py/gui/pages/settings.py
Bossiara13 59a1258dcc feat: Wave 3 — plugin loader + Flet GUI (parity with Rust fork)
Plugin loader (mirrors Rust #29):
- plugins_store.py walks %APPDATA%\com.priler.jarvis\plugins\<name>\
  for commands.yaml fragments and merges them into VA_CMD_LIST at
  startup. Same schema as the project root commands.yaml. Per-pack
  `disabled` flag file skips a pack without deletion. Built-in ids
  win conflicts so a malicious pack can't silently override
  open_browser. 9 unit tests (parity with the Rust tests).
- main.py: plugin merge runs after the root yaml load; logs the
  number of extra commands picked up.

Flet GUI (#27 — first parity with the Rust Tauri GUI):
- New gui/ package: __main__.py launches via `python -m gui` (or
  gui.bat). app.py routes between 7 pages (Главная / Команды /
  Макросы / Расписание / Память / Плагины / Настройки) via
  NavigationRail; dark theme matching the Tauri look.
- gui/services.py centralises every store-module call the pages
  share — single place to swap implementations later.
- One file per page in gui/pages/, all expose `build(page)` and use
  the same card layout idiom. Pages mirror the Tauri ones feature-
  for-feature: home (status + counters + launcher), commands
  (filterable list of all VA_CMD_LIST entries), macros (list +
  delete + recording banner), scheduler (list + remove), memory
  (add/forget/search), plugins (toggle/open folder), settings
  (profile + LLM hot-swap).
- requirements.txt: flet>=0.85 (optional; assistant runs without).
- 3 smoke tests verify imports + build callables; skip cleanly when
  Flet isn't installed.

README: documents both the GUI and the plugin layout in user-facing
terms. Tests: 42 → 54 (+9 plugins +3 GUI smoke).
2026-05-16 13:27:04 +03:00

77 lines
2.8 KiB
Python

"""Settings page — active profile + LLM backend swap."""
import flet as ft
from gui import services as svc
def build(page: ft.Page) -> ft.Control:
profiles = svc.profile_list()
active_profile = svc.profile_active()
try:
llm_b, llm_m = svc.llm_active()
except (RuntimeError, OSError, ValueError) as exc:
llm_b, llm_m = "ошибка", str(exc)
profile_dd = ft.Dropdown(
label="Активный профиль",
options=[ft.dropdown.Option(p) for p in profiles],
value=active_profile,
expand=True,
)
def apply_profile(_):
target = profile_dd.value
if not target:
return
svc.profile_set(target)
page.open(ft.SnackBar(content=ft.Text(f"Профиль: {target}", color="white"), bgcolor="#33d3a4"))
llm_dd = ft.Dropdown(
label="LLM бэкенд",
options=[ft.dropdown.Option("groq"), ft.dropdown.Option("ollama")],
value=llm_b if llm_b in ("groq", "ollama") else "groq",
expand=True,
)
def apply_llm(_):
target = llm_dd.value
if not target:
return
try:
new = svc.llm_swap(target)
page.open(ft.SnackBar(content=ft.Text(f"LLM → {new}", color="white"), bgcolor="#33d3a4"))
except (ValueError, RuntimeError, OSError) as exc:
page.open(ft.SnackBar(content=ft.Text(f"Ошибка: {exc}", color="white"), bgcolor="#d8584f"))
return ft.Column(
[
ft.Text("Настройки", size=24, weight=ft.FontWeight.W_700),
ft.Container(
content=ft.Column([
ft.Text("Профиль", size=14, weight=ft.FontWeight.W_500),
ft.Text(
"Профили ограничивают какие команды активны (например, «sleep» отрубает шумные).",
color="#8aa0a8", size=11,
),
ft.Row([profile_dd, ft.FilledButton(text="Применить", on_click=apply_profile)]),
], spacing=8),
bgcolor="#15252b",
border_radius=8,
padding=14,
),
ft.Container(
content=ft.Column([
ft.Text("LLM", size=14, weight=ft.FontWeight.W_500),
ft.Text(f"Сейчас: {llm_b} · {llm_m}", color="#8aa0a8", size=11),
ft.Row([llm_dd, ft.FilledButton(text="Переключить", on_click=apply_llm)]),
], spacing=8),
bgcolor="#15252b",
border_radius=8,
padding=14,
),
],
spacing=14,
expand=True,
scroll=ft.ScrollMode.AUTO,
)