Spaces:
Running on Zero
A newer version of the Gradio SDK is available: 6.19.0
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
- Source:
- Right: message thread for selected peer
- Auto-refresh on local pubsub topic
chat.message.<our_short_id>
- Auto-refresh on local pubsub topic
- 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 bannerbus.registry.subscribe()for topology pane (additive)- Pubsub
marketplace.post.createdfor 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_presenttest_emergency_tab_hidden_when_onlinetest_emergency_tab_shown_when_offlinetest_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 sequencetest_marketplace_tab_refreshes_on_pubsub_eventtest_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
- Gradio version compatibility β Gradio 6.x evolves quickly. Pin a minor.
- Native mobile β Phase 2 (Flutter or React Native). Web works for hackathon.
- Accessibility β colour contrast meets WCAG AA in both themes; not yet audited.
- Internationalisation β UI strings in German + English. Switchable. Plattdeutsch as a stretch.
- Cytoscape vs D3 β Cytoscape preferred (less code). Performance budget: 50 nodes, 500 edges.