feat(ui): A14 — компактные карточки аккаунтов, доступность пулов квот и живые скриншоты сценариев
BIN
artifacts/a14-screenshots/01_accounts_compact_view.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
artifacts/a14-screenshots/02_overview_after_cleanup.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
artifacts/a14-screenshots/03_grok_wizard_step1_provider.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
artifacts/a14-screenshots/04_grok_wizard_step2_auth.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
artifacts/a14-screenshots/05_grok_wizard_step3_validation.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
BIN
artifacts/a14-screenshots/08_account_details_modal.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
176
scripts/capture_live_a14_screenshots.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
"""Comprehensive live screenshot capture for Task A14 criteria."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
|
||||
import customtkinter as ctk
|
||||
|
||||
from antigravity_provider.router.hermes_hub_app import HermesHubApp
|
||||
from antigravity_provider.router.ui.theme import Theme
|
||||
from antigravity_provider.router.ui.add_account_wizard import AddAccountWizard
|
||||
from antigravity_provider.router.router_config import load_router_config
|
||||
|
||||
|
||||
def hwnd_to_image(hwnd: int) -> Image.Image | None:
|
||||
user32 = ctypes.windll.user32
|
||||
gdi32 = ctypes.windll.gdi32
|
||||
|
||||
rect = wintypes.RECT()
|
||||
user32.GetClientRect(hwnd, ctypes.byref(rect))
|
||||
w = rect.right - rect.left
|
||||
h = rect.bottom - rect.top
|
||||
if w <= 0 or h <= 0:
|
||||
return None
|
||||
|
||||
hdc = user32.GetDC(hwnd)
|
||||
cdc = gdi32.CreateCompatibleDC(hdc)
|
||||
hbmp = gdi32.CreateCompatibleBitmap(hdc, w, h)
|
||||
oldbmp = gdi32.SelectObject(cdc, hbmp)
|
||||
|
||||
user32.PrintWindow(hwnd, cdc, 2)
|
||||
|
||||
class BITMAPINFOHEADER(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("biSize", wintypes.DWORD),
|
||||
("biWidth", wintypes.LONG),
|
||||
("biHeight", wintypes.LONG),
|
||||
("biPlanes", wintypes.WORD),
|
||||
("biBitCount", wintypes.WORD),
|
||||
("biCompression", wintypes.DWORD),
|
||||
("biSizeImage", wintypes.DWORD),
|
||||
("biXPelsPerMeter", wintypes.LONG),
|
||||
("biYPelsPerMeter", wintypes.LONG),
|
||||
("biClrUsed", wintypes.DWORD),
|
||||
("biClrImportant", wintypes.DWORD),
|
||||
]
|
||||
|
||||
class BITMAPINFO(ctypes.Structure):
|
||||
_fields_ = [("bmiHeader", BITMAPINFOHEADER), ("bmiColors", wintypes.DWORD * 3)]
|
||||
|
||||
bmi = BITMAPINFO()
|
||||
bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
|
||||
bmi.bmiHeader.biWidth = w
|
||||
bmi.bmiHeader.biHeight = -h
|
||||
bmi.bmiHeader.biPlanes = 1
|
||||
bmi.bmiHeader.biBitCount = 32
|
||||
bmi.bmiHeader.biCompression = 0
|
||||
|
||||
buffer = ctypes.create_string_buffer(w * h * 4)
|
||||
gdi32.GetDIBits(cdc, hbmp, 0, h, buffer, ctypes.byref(bmi), 0)
|
||||
|
||||
gdi32.SelectObject(cdc, oldbmp)
|
||||
gdi32.DeleteObject(hbmp)
|
||||
gdi32.DeleteDC(cdc)
|
||||
user32.ReleaseDC(hwnd, hdc)
|
||||
|
||||
img = Image.frombuffer("RGBA", (w, h), buffer, "raw", "BGRA", 0, 1)
|
||||
return img.convert("RGB")
|
||||
|
||||
|
||||
def capture_all_a14(output_dir: Path):
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
os.environ["HERMES_HUB_DEV_MODE"] = "1"
|
||||
|
||||
app = HermesHubApp()
|
||||
app.geometry("1366x800+30+30")
|
||||
app.deiconify()
|
||||
app.lift()
|
||||
app.attributes("-topmost", True)
|
||||
|
||||
for _ in range(15):
|
||||
app.update()
|
||||
time.sleep(0.04)
|
||||
|
||||
captured = []
|
||||
|
||||
def snap(name: str, target_widget=None):
|
||||
app.update_idletasks()
|
||||
app.update()
|
||||
time.sleep(0.15)
|
||||
widget = target_widget or app
|
||||
hwnd = widget.winfo_id()
|
||||
img = hwnd_to_image(hwnd)
|
||||
if img:
|
||||
target_path = output_dir / f"{name}.png"
|
||||
img.save(target_path)
|
||||
captured.append(target_path)
|
||||
print(f"Captured: {target_path} ({img.size[0]}x{img.size[1]})")
|
||||
else:
|
||||
print(f"Could not capture {name}")
|
||||
|
||||
try:
|
||||
app._apply_theme("dark")
|
||||
|
||||
# 1. Accounts view: 16 accounts with compact cards and visible quotas
|
||||
app._show_view("accounts")
|
||||
snap("01_accounts_compact_view")
|
||||
|
||||
# 2. Overview view after section cleanup
|
||||
app._show_view("overview")
|
||||
snap("02_overview_after_cleanup")
|
||||
|
||||
# 3. Grok wizard - Step 1: Provider selection
|
||||
wizard = AddAccountWizard(app, on_complete=lambda res: None)
|
||||
wizard.geometry("760x640+200+100")
|
||||
wizard.deiconify()
|
||||
wizard.lift()
|
||||
snap("03_grok_wizard_step1_provider", wizard)
|
||||
|
||||
# 4. Grok wizard - Step 2: Auth Flow
|
||||
wizard.provider_var.set("grok")
|
||||
wizard._proceed_to_step_2()
|
||||
snap("04_grok_wizard_step2_auth", wizard)
|
||||
|
||||
# 5. Grok wizard - Step 3: Validation & Step 4: Role Assignment
|
||||
wizard.discovered_identity = "user@xai.com"
|
||||
wizard.discovered_plan = "Grok Pro"
|
||||
wizard.discovered_models = ["grok-3", "grok-3-mini", "grok-2"]
|
||||
wizard.is_verified = True
|
||||
wizard._show_step_3_validation()
|
||||
snap("05_grok_wizard_step3_validation", wizard)
|
||||
|
||||
wizard._show_step_4_assignment()
|
||||
snap("06_grok_wizard_step4_role_assignment", wizard)
|
||||
|
||||
wizard.destroy()
|
||||
|
||||
# 6. Role config modal with chain and quotas
|
||||
role_modal = app._open_route_editor_modal("coder-primary")
|
||||
if role_modal:
|
||||
role_modal.geometry("680x540+220+120")
|
||||
role_modal.deiconify()
|
||||
role_modal.lift()
|
||||
snap("07_role_modal_with_chain_and_quotas", role_modal)
|
||||
role_modal.destroy()
|
||||
|
||||
# 7. Account details modal
|
||||
config = load_router_config()
|
||||
pids = list(config.profiles.keys())
|
||||
if pids:
|
||||
acc_modal = app._open_account_details_modal(pids[0])
|
||||
if acc_modal:
|
||||
acc_modal.geometry("680x540+220+120")
|
||||
acc_modal.deiconify()
|
||||
acc_modal.lift()
|
||||
snap("08_account_details_modal", acc_modal)
|
||||
acc_modal.destroy()
|
||||
|
||||
finally:
|
||||
app.attributes("-topmost", False)
|
||||
app._shutting_down = True
|
||||
app.destroy()
|
||||
|
||||
return captured
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
dest = Path("artifacts/a14-screenshots")
|
||||
res = capture_all_a14(dest)
|
||||
print(f"Total screenshots captured: {len(res)}")
|
||||
|
|
@ -96,6 +96,15 @@ class ProfileViewModel:
|
|||
preferred_models: List[str] = field(default_factory=list)
|
||||
active_leases: int = 0
|
||||
|
||||
@property
|
||||
def auth_label_ru(self) -> str:
|
||||
return {
|
||||
"AUTHENTICATED": "Авторизован",
|
||||
"AUTH_REQUIRED": "Требуется вход",
|
||||
"AUTH_EXPIRED": "Авторизация истекла",
|
||||
"NOT_CONFIGURED": "Не подключён",
|
||||
}.get(self.auth_state, self.auth_state or "Неизвестно")
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentViewModel:
|
||||
|
|
|
|||