GitHub Actions
Add all-to-all internet mesh over relay hub (P1-P3) + user-story screenshot proof
8f53c4c
|
Raw
History Blame Contribute Delete
10.8 kB

A newer version of the Gradio SDK is available: 6.19.0

Upgrade

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

# 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

# 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 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:

--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. 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) instead of showing tabs.


9. Configuration

From X04 Β§3:

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
Emergency state source M09 Β§3.1
Pubsub topics CONTRACT Β§8
Onboarding flow M13
Mobile served by X01 Β§3.2
Trace event format M03 Β§3.6

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.