J.A.R.V.I.S-py/gui/pages/home.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

119 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Home page — quick launcher + active backends + counters."""
import flet as ft
from gui import services as svc
def _stat_card(title: str, value: str, color: str = "#33d3a4") -> ft.Container:
return ft.Container(
content=ft.Column(
[
ft.Text(title, size=11, color="#8aa0a8"),
ft.Text(value, size=22, weight=ft.FontWeight.W_600, color=color),
],
spacing=2,
),
bgcolor="#15252b",
border_radius=10,
padding=14,
expand=True,
)
def build(page: ft.Page) -> ft.Control:
running = svc.assistant_is_running()
try:
llm_b, llm_m = svc.llm_active()
except (RuntimeError, OSError, ValueError) as exc:
llm_b, llm_m = "", str(exc)
counters = ft.Row(
controls=[
_stat_card("Команды", str(_safe_count(svc, "commands"))),
_stat_card("Макросы", str(len(svc.macros_all()))),
_stat_card("Расписание", str(len(svc.scheduler_all()))),
_stat_card("Память (фактов)", str(len(svc.memory_all()))),
_stat_card("Плагины", str(len(svc.plugins_all()))),
],
spacing=10,
)
status_chip = ft.Container(
content=ft.Text(
"АССИСТЕНТ РАБОТАЕТ" if running else "АССИСТЕНТ ОСТАНОВЛЕН",
size=12,
weight=ft.FontWeight.W_700,
color="#13191c",
),
bgcolor="#33d3a4" if running else "#d8d8d8",
padding=ft.padding.symmetric(horizontal=12, vertical=6),
border_radius=20,
)
start_btn = ft.FilledButton(
text="Запустить J.A.R.V.I.S.",
icon=ft.Icons.PLAY_ARROW,
on_click=lambda e: _launch(page, e),
disabled=running,
)
backends_card = ft.Container(
content=ft.Column([
ft.Text("Активный LLM", size=11, color="#8aa0a8"),
ft.Text(f"{llm_b} · {llm_m}", size=14, weight=ft.FontWeight.W_500),
], spacing=2),
bgcolor="#15252b",
border_radius=10,
padding=14,
)
return ft.Column(
controls=[
ft.Row([
ft.Text("J.A.R.V.I.S.", size=28, weight=ft.FontWeight.W_700),
status_chip,
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
ft.Text(
"Голосовой ассистент — Python-форк. Эта панель управляет состоянием, "
"запускает main.py и переключает движки.",
color="#8aa0a8",
size=12,
),
ft.Divider(color="#1f3540"),
backends_card,
counters,
ft.Divider(color="#1f3540"),
start_btn,
],
spacing=14,
expand=True,
scroll=ft.ScrollMode.AUTO,
)
def _launch(page: ft.Page, _e) -> None:
ok = svc.assistant_run()
page.open(
ft.SnackBar(
content=ft.Text(
"Запускаю main.py" if ok else "Не удалось запустить main.py",
color="white",
),
bgcolor="#33d3a4" if ok else "#d8584f",
)
)
def _safe_count(svc_module, kind: str) -> int:
"""Counts come from disk; if a store fails we display 0 rather than crash."""
try:
if kind == "commands":
# Don't import the full VA_CMD_LIST machinery — read yaml directly.
import os, yaml # pylint: disable=import-outside-toplevel
here = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
with open(os.path.join(here, "commands.yaml"), "rt", encoding="utf8") as f:
return len(yaml.safe_load(f) or {})
except (OSError, ValueError):
return 0
return 0