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.