Spaces:
Running on Zero
Running on Zero
GitHub Actions
Add all-to-all internet mesh over relay hub (P1-P3) + user-story screenshot proof
8f53c4c | # M08 — UI (Gradio Dashboard + Mobile Client) | |
| **Spec version:** v1.0 | |
| **Depends on:** M03 (bus, the ONLY data source the UI talks to), X03 (observability, for trace display), X04 (config), M09 (emergency state subscribed), `gradio>=6.0.0` | |
| **Depended on by:** M13 (onboarding extends the UI), M12 (CLI may launch UI) | |
| The UI's strict rule: **it never imports a service module**. Every piece of data comes via `bus.call(...)` or via the bus's introspection APIs (`topology_snapshot`, `recent_traces`). This keeps the UI swappable. | |
| --- | |
| ## 1. Responsibility | |
| Present a local-host web UI at `http://127.0.0.1:7860` showing: | |
| - Live topology of the mesh | |
| - An "ask" pane wired to `llm.chat` + `rag.query` | |
| - A chat tab for direct messages | |
| - A marketplace tab | |
| - A files tab | |
| - An emergency tab (visible only when offline) | |
| - A settings tab | |
| - A mobile web client served at `/mobile` | |
| --- | |
| ## 2. File layout | |
| ``` | |
| hearthnet/ui/ | |
| ├── __init__.py | |
| ├── app.py # build_ui(): assembles Gradio Blocks | |
| ├── topology.py # Cytoscape.js-backed topology component | |
| ├── theme.py # Colour tokens, fonts, CSS | |
| ├── onboarding.py # M13 owns this; reachable from settings | |
| ├── tabs/ | |
| │ ├── __init__.py | |
| │ ├── ask.py # LLM passthrough with optional RAG | |
| │ ├── chat.py # direct messages | |
| │ ├── marketplace.py | |
| │ ├── files.py | |
| │ ├── emergency.py # only mounted when offline state active | |
| │ └── settings.py | |
| └── mobile/ # served as static at /mobile | |
| ├── index.html | |
| ├── app.js | |
| └── style.css | |
| ``` | |
| --- | |
| ## 3. Public API | |
| ### 3.1 `app.py` | |
| ```python | |
| # hearthnet/ui/app.py | |
| import gradio as gr | |
| class UiApp: | |
| def __init__( | |
| self, | |
| bus: CapabilityBus, | |
| state_bus: StateBus, # M09 | |
| config: UiConfig, | |
| node_id_short: str, | |
| community_name: str, | |
| ): | |
| ... | |
| def build(self) -> gr.Blocks: | |
| """Assemble the full UI.""" | |
| async def launch_async(self) -> None: | |
| """Non-blocking launch. Used by node.py.""" | |
| async def shutdown(self) -> None: ... | |
| def build_ui(bus, state_bus, config, **meta) -> UiApp: | |
| """Convenience constructor used by node.py.""" | |
| ``` | |
| ### 3.2 `topology.py` | |
| ```python | |
| # hearthnet/ui/topology.py | |
| class TopologyComponent: | |
| """Wraps Cytoscape.js inside a Gradio HTML component. | |
| Auto-refreshes from bus.topology_snapshot() every 2s. | |
| Animates recent trace events (last 10s) along edges.""" | |
| def __init__(self, bus: CapabilityBus): ... | |
| def render(self) -> gr.HTML: ... | |
| def push_trace(self, event: CallTraceEvent) -> None: | |
| """Trigger an edge animation. Color by capability prefix.""" | |
| def push_topology(self, snapshot: TopologySnapshot) -> None: ... | |
| # Cytoscape config: | |
| # - Nodes: one per known peer + one for self | |
| # - Edges: dynamic; appear on trace events; fade after 5s | |
| # - Edge colour: llm.*=teal, rag.*=purple, file.*=amber, | |
| # chat.*=blue, market.*=green, community.*=grey | |
| # - Node colour: online=green, stale=amber, offline=red | |
| # - Node label: display_name + capability badges | |
| # - On node click: side panel shows full manifest | |
| ``` | |
| --- | |
| ## 4. Composition | |
| The Gradio Blocks tree: | |
| ``` | |
| gr.Blocks(theme=hearthnet_theme, title="HearthNet") | |
| ├── header bar | |
| │ ├── community name + node display name | |
| │ ├── status pill (online/offline) → bound to state_bus | |
| │ └── settings gear | |
| ├── topology pane (always visible at top) | |
| └── tabs: | |
| ├── Ask (always) | |
| ├── Chat (always; badge with unread count) | |
| ├── Marketplace (always) | |
| ├── Files (always) | |
| ├── Notfall (visible only when state.mode != "online") | |
| └── Settings (always; includes Onboarding entry point) | |
| ``` | |
| --- | |
| ## 5. Tabs | |
| ### 5.1 Ask tab — `tabs/ask.py` | |
| A simple chat interface: | |
| - Top: Corpus selector (dropdown, populated via `bus.call("rag.list_corpora", ...)`) | |
| - Top right: Model selector (capabilities from `bus.topology_snapshot().capabilities_*` filtered by name=`llm.chat`) | |
| - Centre: Chat history (Gradio Chatbot) | |
| - Bottom: Input + Send | |
| Behaviour on send: | |
| ``` | |
| 1. if corpus selected: | |
| chunks = bus.call("rag.query", (1,0), {params:{corpus}, input:{query:msg, k:5}}) | |
| build system prompt with chunks + sources | |
| 2. messages = [system_with_chunks, ...history, user_msg] | |
| 3. stream = bus.stream("llm.chat", (1,0), {params:{model}, input:{messages, stream:true}}) | |
| 4. accumulate tokens into a streaming response in the Chatbot | |
| 5. on done: append sources panel (clickable to open file) | |
| ``` | |
| ### 5.2 Settings tab — `tabs/settings.py` | |
| - Node identity (read-only) | |
| - Community membership (read-only; "leave community" with double-confirm) | |
| - LLM backend list (read-only; edit via config.toml) | |
| - Theme toggle (Hearth / Spark dark mode) | |
| - Debug toggles (verbose logging, trace ring buffer dump) | |
| - Onboarding entrypoints: "Create new community", "Join via invite" | |
| - Privacy: "Erase all data" (triple-confirm, wipes keys + state) | |
| ### 5.3 Chat tab — `tabs/chat.py` | |
| - Left: peer list with last-message timestamps, unread badges | |
| - Source: `bus.call("chat.history", (1,0), {input:{}})` → group by peer | |
| - Right: message thread for selected peer | |
| - Auto-refresh on local pubsub topic `chat.message.<our_short_id>` | |
| - Bottom: input + send + attachment button (opens file picker → uploads as blob via `file.put` → attaches CID) | |
| - "Encrypted" indicator placeholder (Phase 2) | |
| ### 5.4 Marketplace tab — `tabs/marketplace.py` | |
| - Top: Category filter, tag filter, search box (semantic) | |
| - Centre: Cards (one per post) | |
| - Bottom-right: "Neuer Beitrag" → modal for new post | |
| - "Mark fulfilled" / "Withdraw" on each card if author == us | |
| ### 5.5 Files tab — `tabs/files.py` | |
| - Left: corpus / pinned / recent | |
| - Centre: file grid | |
| - Upload area at top | |
| - Click: preview (image / PDF / text), download, advertise to peers | |
| ### 5.6 Emergency tab — `tabs/emergency.py` | |
| Visible only when `state_bus.current().mode != "online"`. Designed for big buttons, low-stress reading. Large amber banner at top. | |
| Contents: | |
| - Big "Was tun?" button → opens the most relevant corpus (default `niederrhein-emergency`) | |
| - Neighbour list (last seen times prominent) | |
| - Direct chat shortcut | |
| - "Update" indicator: how far behind the event log we are vs. last sync | |
| - Shared resources table (generator availability, water, light) — Phase 2 | |
| ### 5.7 Banner | |
| ``` | |
| INTERNET OFFLINE — LOKAL AKTIV | |
| seit 14:32 · 3 Nachbar*innen erreichbar | |
| ``` | |
| When degraded: | |
| ``` | |
| EINGESCHRÄNKTE VERBINDUNG · Lokale Dienste aktiv | |
| ``` | |
| --- | |
| ## 6. Mobile client (`mobile/`) | |
| Plain static HTML + JS, no framework. Served by [X01](../cross-cutting/X01-transport.md) at `/mobile/*`. Same bus API (signed requests, but credentials stored in `IndexedDB`). | |
| Minimum features: | |
| - Ask (LLM passthrough) | |
| - Chat | |
| - Marketplace browse | |
| - Emergency mode banner | |
| - No topology viz (too dense for small screen) | |
| Auth on mobile: the user scans an invite QR with the camera → key derived in WebCrypto → stored in IndexedDB. | |
| --- | |
| ## 7. Theming (`theme.py`) | |
| Two themes: | |
| - **Hearth (default)** — warm, parchment background, dark walnut accents | |
| - **Spark (high-contrast / dark)** — black bg, amber accents — also the emergency theme | |
| CSS variables: | |
| ```css | |
| --hn-bg: #f4ead7; /* hearth */ | |
| --hn-bg-dark: #1a1816; /* spark */ | |
| --hn-accent: #b45309; /* amber */ | |
| --hn-accent-2: #14b8a6; /* teal */ | |
| --hn-accent-3: #6d28d9; /* purple, used for rag */ | |
| --hn-text: #2c1810; | |
| --hn-text-dark: #f4ead7; | |
| --hn-error: #b91c1c; | |
| --hn-warn: #d97706; | |
| --hn-ok: #15803d; | |
| ``` | |
| When emergency mode is active, theme switches to Spark with amber accents and the banner. | |
| --- | |
| ## 8. Behaviour | |
| ### 8.1 Topology refresh | |
| Every 2 s the topology component calls `bus.topology_snapshot()`. Diff with previous; only changed nodes/edges trigger re-render. Trace ring is read via `bus.recent_traces(50)` every 1 s and pushed as animations. | |
| ### 8.2 Live updates without polling | |
| Where possible the UI subscribes: | |
| - `state_bus.subscribe()` for emergency banner | |
| - `bus.registry.subscribe()` for topology pane (additive) | |
| - Pubsub `marketplace.post.created` for marketplace tab live refresh | |
| - Pubsub `chat.message.<our_short_id>` for chat tab notifications | |
| ### 8.3 Error display | |
| - Capability call errors → toast at top with code and "details" expander | |
| - Backend warm-up takes time → spinner with "Modell wird geladen ..." | |
| - Network failures during a stream → frame "verbindung abgerissen" injected; user can retry | |
| ### 8.4 Settings persistence | |
| Settings tab edits go to `config.toml` via [X04 §3](../cross-cutting/X04-config.md). Some require restart; UI clearly indicates this. | |
| ### 8.5 First-run handoff to M13 | |
| If on startup `config.community.community_id is None`, the UI redirects to onboarding (see [M13](M13-onboarding.md)) instead of showing tabs. | |
| --- | |
| ## 9. Configuration | |
| From [X04 §3](../cross-cutting/X04-config.md): | |
| ```python | |
| config.ui.host # 127.0.0.1 | |
| config.ui.port # 7860 | |
| config.ui.launch_browser # auto-open in browser on launch | |
| ``` | |
| --- | |
| ## 10. Tests | |
| ### Unit | |
| - `test_theme_tokens_present` | |
| - `test_emergency_tab_hidden_when_online` | |
| - `test_emergency_tab_shown_when_offline` | |
| - `test_topology_diff_avoids_unchanged_render` (mock bus) | |
| - `test_settings_writes_to_config_file_atomically` | |
| ### Integration | |
| - `test_ask_tab_does_rag_then_llm_in_order` — mock bus, observe call sequence | |
| - `test_marketplace_tab_refreshes_on_pubsub_event` | |
| - `test_mobile_endpoint_serves_index_html` | |
| ### Manual | |
| - Demo dry-run script: open UI, type query, observe topology animation, unplug WAN, observe banner ≤ 5s. Document in `tests/demo_script.md`. | |
| --- | |
| ## 11. Cross-references | |
| | What | Where | | |
| |------|-------| | |
| | Bus introspection APIs | [M03 §3.7](M03-bus.md) | | |
| | Emergency state source | [M09 §3.1](M09-emergency.md) | | |
| | Pubsub topics | [CONTRACT §8](../CAPABILITY_CONTRACT.md) | | |
| | Onboarding flow | [M13](M13-onboarding.md) | | |
| | Mobile served by | [X01 §3.2](../cross-cutting/X01-transport.md) | | |
| | Trace event format | [M03 §3.6](M03-bus.md) | | |
| --- | |
| ## 12. Open questions | |
| 1. **Gradio version compatibility** — Gradio 6.x evolves quickly. Pin a minor. | |
| 2. **Native mobile** — Phase 2 (Flutter or React Native). Web works for hackathon. | |
| 3. **Accessibility** — colour contrast meets WCAG AA in both themes; not yet audited. | |
| 4. **Internationalisation** — UI strings in German + English. Switchable. Plattdeutsch as a stretch. | |
| 5. **Cytoscape vs D3** — Cytoscape preferred (less code). Performance budget: 50 nodes, 500 edges. | |