Spaces:
Running on Zero
Running on Zero
File size: 10,777 Bytes
6f9a5fd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 | # 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.
|