Spaces:
Running on Zero
Running on Zero
GitHub Actions
feat: mesh graph tab, routing trace, QR invite, remove all fakes, specialized-node tests
7573216 | """Generate HearthNet UI screenshots. | |
| Launches a two-node mesh, performs real interactions (LLM query, chat send, | |
| peer list refresh, marketplace post, RAG ingest), then saves screenshots. | |
| Usage: python scripts/gen_screenshots.py | |
| Output: docs/screenshots/*.png | |
| """ | |
| from __future__ import annotations | |
| import socket | |
| import threading | |
| import time | |
| import urllib.request | |
| from pathlib import Path | |
| OUT = Path("docs/screenshots") | |
| OUT.mkdir(parents=True, exist_ok=True) | |
| def _free_port() -> int: | |
| s = socket.socket() | |
| s.bind(("127.0.0.1", 0)) | |
| p = s.getsockname()[1] | |
| s.close() | |
| return p | |
| def _wait_ready(port: int, timeout: float = 20.0) -> None: | |
| deadline = time.time() + timeout | |
| while time.time() < deadline: | |
| try: | |
| urllib.request.urlopen(f"http://127.0.0.1:{port}/", timeout=2) | |
| return | |
| except Exception: | |
| time.sleep(0.4) | |
| raise TimeoutError(f"port {port} not ready after {timeout}s") | |
| def _launch_node(demo, port: int) -> None: | |
| """Launch a pre-built Gradio Blocks on the given port.""" | |
| demo.launch( | |
| server_name="127.0.0.1", | |
| server_port=port, | |
| prevent_thread_lock=True, | |
| quiet=True, | |
| ) | |
| def main() -> None: | |
| from hearthnet.node import InMemoryNetwork | |
| net = InMemoryNetwork() | |
| alice = net.add_node("alice", "Alice", "ed25519:hearthnet-demo") | |
| bob = net.add_node("bob", "Bob", "ed25519:hearthnet-demo") | |
| alice.install_demo_services(corpus="alice-docs") | |
| bob.install_demo_services(corpus="bob-docs") | |
| net.mesh_discover() | |
| port_a = _free_port() | |
| port_b = _free_port() | |
| from hearthnet.ui.app import build_ui | |
| # Build UIs sequentially (Gradio's block context is not thread-safe) | |
| def _build(node): | |
| return build_ui( | |
| bus=node.bus, | |
| state_bus=node.state_bus, | |
| display_name=node.display_name, | |
| node_id=node.node_id, | |
| community_id=node.community_id, | |
| ).build() | |
| demo_a = _build(alice) | |
| demo_b = _build(bob) | |
| threading.Thread(target=_launch_node, args=(demo_a, port_a), daemon=True).start() | |
| threading.Thread(target=_launch_node, args=(demo_b, port_b), daemon=True).start() | |
| _wait_ready(port_a) | |
| _wait_ready(port_b) | |
| time.sleep(1.5) # let JS hydrate | |
| from playwright.sync_api import sync_playwright | |
| with sync_playwright() as pw: | |
| browser = pw.chromium.launch(headless=True) | |
| def _goto(page, url="/"): | |
| page.goto(url, wait_until="networkidle", timeout=30_000) | |
| def _click_tab(page, name): | |
| page.get_by_role("tab", name=name).click() | |
| page.wait_for_load_state("networkidle", timeout=15_000) | |
| # ββ Alice ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| ctx_a = browser.new_context( | |
| base_url=f"http://127.0.0.1:{port_a}", | |
| viewport={"width": 1280, "height": 900}, | |
| ) | |
| page_a = ctx_a.new_page() | |
| _goto(page_a) | |
| # 1. Ask tab β send a message and capture response | |
| page_a.screenshot(path=str(OUT / "01-alice-ask-empty.png")) | |
| print(" 01-alice-ask-empty.png β") | |
| # Gradio renders textareas inside shadow DOM; use .first for the message box | |
| textarea = page_a.locator("textarea").first | |
| textarea.fill("What is HearthNet? Explain in two sentences.") | |
| # Multi-line textarea: click Send button (Enter adds newline, not submit) | |
| page_a.get_by_role("button", name="Send").first.click() | |
| page_a.wait_for_timeout(4000) | |
| page_a.screenshot(path=str(OUT / "02-alice-ask-response.png")) | |
| print(" 02-alice-ask-response.png β") | |
| # 2. Chat tab β send a message to bob | |
| _click_tab(page_a, "Chat") | |
| # First textbox = recipient, second textbox = message | |
| textboxes = page_a.get_by_role("textbox") | |
| try: | |
| textboxes.nth(0).fill("bob") | |
| textboxes.nth(1).fill("Hey Bob, can you hear me?") | |
| page_a.get_by_role("button", name="Send").click() | |
| page_a.wait_for_timeout(1500) | |
| except Exception as exc: | |
| print(f" Chat fill failed: {exc}") | |
| page_a.screenshot(path=str(OUT / "03-alice-chat.png")) | |
| print(" 03-alice-chat.png β") | |
| # 3. Marketplace tab | |
| _click_tab(page_a, "Marketplace") | |
| page_a.screenshot(path=str(OUT / "04-alice-marketplace.png")) | |
| print(" 04-alice-marketplace.png β") | |
| # 4. Files tab | |
| _click_tab(page_a, "Files") | |
| page_a.screenshot(path=str(OUT / "05-alice-files.png")) | |
| print(" 05-alice-files.png β") | |
| # 5. Emergency tab | |
| _click_tab(page_a, "Emergency") | |
| page_a.screenshot(path=str(OUT / "06-alice-emergency.png")) | |
| print(" 06-alice-emergency.png β") | |
| # 6. Settings tab β shows alice node ID and bob as peer | |
| _click_tab(page_a, "Settings") | |
| page_a.screenshot(path=str(OUT / "07-alice-settings.png")) | |
| print(" 07-alice-settings.png β") | |
| # Click "Refresh Peers" to see bob in the list | |
| try: | |
| page_a.get_by_role("button", name="Refresh Peers").click() | |
| page_a.wait_for_timeout(2000) | |
| page_a.screenshot(path=str(OUT / "08-alice-settings-peers.png")) | |
| print(" 08-alice-settings-peers.png β") | |
| except Exception as exc: | |
| print(f" Peers refresh failed: {exc}") | |
| # 7. Mesh tab β refresh to show topology with Alice + Bob | |
| _click_tab(page_a, "Mesh") | |
| page_a.screenshot(path=str(OUT / "08b-alice-mesh-before-refresh.png")) | |
| print(" 08b-alice-mesh-before-refresh.png β") | |
| try: | |
| page_a.get_by_role("button", name="Refresh Mesh").click() | |
| page_a.wait_for_timeout(2000) | |
| page_a.screenshot(path=str(OUT / "08c-alice-mesh-live.png")) | |
| print(" 08c-alice-mesh-live.png β") | |
| except Exception as exc: | |
| print(f" Mesh refresh failed: {exc}") | |
| ctx_a.close() | |
| # ββ Bob ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| ctx_b = browser.new_context( | |
| base_url=f"http://127.0.0.1:{port_b}", | |
| viewport={"width": 1280, "height": 900}, | |
| ) | |
| page_b = ctx_b.new_page() | |
| _goto(page_b) | |
| page_b.screenshot(path=str(OUT / "09-bob-ask-tab.png")) | |
| print(" 09-bob-ask-tab.png β") | |
| # Bob Ask β ask a question | |
| textarea_b = page_b.locator("textarea").first | |
| textarea_b.fill("Hello from Bob! What can I do on HearthNet?") | |
| page_b.get_by_role("button", name="Send").first.click() | |
| page_b.wait_for_timeout(4000) | |
| page_b.screenshot(path=str(OUT / "09b-bob-ask-response.png")) | |
| print(" 09b-bob-ask-response.png β") | |
| # Bob mesh tab β should show Alice | |
| _click_tab(page_b, "Mesh") | |
| try: | |
| page_b.get_by_role("button", name="Refresh Mesh").click() | |
| page_b.wait_for_timeout(2000) | |
| page_b.screenshot(path=str(OUT / "10-bob-mesh-sees-alice.png")) | |
| print(" 10-bob-mesh-sees-alice.png β") | |
| except Exception as exc: | |
| print(f" Bob mesh refresh failed: {exc}") | |
| # Bob settings β should show alice as peer | |
| _click_tab(page_b, "Settings") | |
| page_b.screenshot(path=str(OUT / "10b-bob-settings.png")) | |
| print(" 10b-bob-settings.png β") | |
| # Refresh Bob's peer list β should show Alice | |
| try: | |
| page_b.get_by_role("button", name="Refresh Peers").click() | |
| page_b.wait_for_timeout(2000) | |
| page_b.screenshot(path=str(OUT / "10c-bob-settings-peers.png")) | |
| print(" 10c-bob-settings-peers.png β") | |
| except Exception as exc: | |
| print(f" Bob peers refresh failed: {exc}") | |
| ctx_b.close() | |
| browser.close() | |
| print("\nScreenshots saved to docs/screenshots/:") | |
| for f in sorted(OUT.glob("*.png")): | |
| print(f" {f.name}") | |
| if __name__ == "__main__": | |
| main() | |