docs(frontend): design del frontend ThothII (brainstorming)

Decisioni FE-1..FE-6: Vite+React SPA (no Next), TanStack Query+Zustand+hook SSE,
EventSource nativo (MVP auth=none), widget registry+fallback, test Vitest+RTL+MSW
+ Playwright e2e F1, build a slice con F1 come primo loop chiuso.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 11:51:14 +02:00
co-authored by Claude Opus 4.8
parent 7efa88aaca
commit 0fe2f2139e
@@ -0,0 +1,160 @@
# ThothII — Design del frontend
**Data:** 2026-06-27
**Stato:** Draft, in attesa di review
**Fonti:** `docs/superpowers/specs/2026-06-25-thothii-architecture-design.md` (§6 UI, §4 widget-descriptor), `docs/superpowers/specs/2026-06-27-backend-design.md` (contratto REST+SSE), backend implementato in `backend/` (route reali).
---
## 1. Obiettivo e contesto
Terzo e ultimo progetto di ThothII (harness → backend → **frontend**). È l'interfaccia React che sostituisce il TUI di ChironeWp3: presenta il workflow NL→SQL human-in-the-loop a 8 fasi, renderizzando i **widget-descriptor** emessi dall'harness e instradati dal backend via SSE, e raccogliendo le decisioni del revisore via REST.
Il backend è già completo e ne espone un contratto stabile e testato. Il frontend **consuma solo** quel contratto (REST + SSE su localhost); non parla mai direttamente con Pi, l'harness o il DB.
**Contratto del backend (reale, implementato):**
- REST: `GET /workspaces`, `GET /models`, `GET /sessions`, `GET /sessions/:id`, `POST /sessions`, `POST /sessions/:id/response`, `POST /sessions/:id/steer`, `POST /sessions/:id/sql/preview`, `POST /sessions/:id/sql/export`, `POST /sessions/:id/close`, `POST /sessions/:id/resume`.
- SSE: `GET /sessions/:id/events` → eventi `{type:"ui_request", ui_request}`, `{type:"info", level, text}`, `{type:"text_delta", text}`, `{type:"system_event", ...}`.
- Widget-descriptor (architettura §4): 6 `kind` (`info`, `select`, `multiselect`, `freetext`, `artifact-gate`, `artifact`) + fallback per kind sconosciuti; opzioni riservate (`back`/`exit`/`other`), `recommended`, linkage `option.opens`, invariante no-limbo.
---
## 2. Decisioni del frontend (locked)
Decise durante il brainstorming del 2026-06-27.
**FE-1 — Vite + React + TypeScript SPA.**
App single-page client-only su localhost; niente SSR/route server. ShadCn per i componenti UI, AGGrid (community) per le tabelle risultati.
Perché: il frontend è un client che consuma un backend Fastify separato; SSR e routing server di Next.js non servono. Vite dà build/dev più semplici e un solo runtime. Deviazione consapevole dall'architettura (che indicava Next.js), coerente con YAGNI; il contratto FE↔BE non cambia.
**FE-2 — Dati: TanStack Query (REST) + Zustand (live) + hook SSE.**
TanStack Query per le chiamate REST (cache, loading/error, refetch di lista sessioni e artefatti). Uno store Zustand per lo stato live della sessione (widget pendente, trascritto chat, info/toast, fase corrente). Un hook `useSessionStream` apre l'SSE e alimenta lo store.
Perché: separazione pulita REST (cacheable) vs live (streaming), poca boilerplate per un'app mono-operatore.
**FE-3 — SSE via `EventSource` nativo nell'MVP.**
`EventSource` (GET, nessun header) basta con auth=`none` su localhost.
Perché: minimale e nativo. Quando si abiliterà auth con header (mock/oidc) servirà un SSE basato su `fetch` (es. `@microsoft/fetch-event-source`) — registrato come item futuro, non MVP.
**FE-4 — Widget registry + fallback universale.**
Un registry mappa `widget.kind` → componente renderer; un fallback mostra il payload JSON in un box "Widget non supportato (kind: X) — rispondi manualmente". Aggiungere un widget = registrare un nuovo renderer, senza toccare l'infrastruttura (forwarding, correlazione `id`, store).
Perché: rispecchia la flessibilità del contratto (architettura §4, `kind` aperto); isola ogni widget come unità testabile.
**FE-5 — Strategia di test: Vitest+RTL+MSW (unit/integration) + Playwright e2e (F1).**
Vitest + React Testing Library per renderer, hook e store; MSW mocka le REST e un mock di `EventSource` simula l'SSE (deterministico, senza backend). Un test Playwright e2e del loop F1 contro il backend reale avviato col `fake-pi-rpc` (analogo all'e2e del backend).
Perché: i renderer si testano in isolamento in modo deterministico; l'e2e prova il rendering reale e il loop end-to-end. CI-friendly.
**FE-6 — Build a vertical slice, F1 come primo loop chiuso.**
Si costruisce per slice (vedi §7), chiudendo il loop F1 end-to-end il prima possibile, poi estendendo widget e viewer.
Perché: feedback continuo, coerente con la strategia D9 dell'architettura.
---
## 3. Architettura e flusso dei dati
```
┌─ FRONTEND (frontend/) — Vite + React SPA, localhost ───────────────────┐
│ app/ provider (QueryClient), composizione, routing minimale │
│ shell/ layout a 4 zone │
│ widgets/ registry kind→renderer (6 + fallback) │
│ viewers/ schema-linking · cte/sql · risultati · markdown-mermaid │
│ store/ sessionStore (Zustand): widget pendente, chat, info, fase │
│ stream/ useSessionStream (EventSource → store) │
│ api/ client REST tipato + tipi del contratto widget-descriptor │
└───────────────▲───────────────────────────────┬───────────────────────┘
SSE (EventSource) REST (fetch / TanStack Query)
│ │
GET /sessions/:id/events POST /sessions/:id/response, /steer,
(ui_request|info| /sql/preview, /sql/export, /close,
text_delta|system_event) /resume · GET /sessions, /:id, ...
└───────────────► BACKEND (Fastify, :8787) ◄─────────────┘
```
**Flusso di una decisione (es. F4 promozione tabelle):**
1. L'utente apre una sessione → `useSessionStream` apre l'SSE.
2. Arriva `{type:"ui_request", ui_request:{id, widget:"multiselect", ...}}` → store salva il widget pendente.
3. Il registry renderizza il `multiselect`; l'utente seleziona e conferma.
4. `POST /sessions/:id/response` con `{ui_response:{id, choices, decision}}`.
5. Il backend instrada a Pi; arrivano nuovi `text_delta` / il prossimo `ui_request`.
**Riconnessione/resume:** alla (ri)apertura dell'SSE il backend ri-emette il widget pendente (già implementato lato BE-3); lo store ricostruisce lista/artefatti via REST. Lo steering ambientale (`!`) → `POST /sessions/:id/steer`.
---
## 4. Componenti e confini
### 4.1 `api/` — client REST + tipi
Un modulo per gruppo (`sessions.ts`, `workspaces.ts`, `models.ts`, `sql.ts`) con funzioni tipate che chiamano il backend. `types.ts` definisce i tipi condivisi del contratto: `UiRequest`, `UiResponse`, `WidgetDescriptor` (union sui 6 kind), `SessionSummary`, `SessionDetail`, `PreviewResult`. È l'unico confine verso il backend.
### 4.2 `store/sessionStore.ts` — stato live (Zustand)
Tiene: `pendingWidget: WidgetDescriptor | null`, `transcript: ChatEntry[]` (accumulo dei `text_delta`), `toasts: Info[]`, `currentPhase`, `connectionState`. Azioni: `applyEvent(event)` (dispatch per tipo), `clearPending()`, reset all'apertura sessione. Nessuna logica di rete qui (solo stato).
### 4.3 `stream/useSessionStream.ts`
Hook che, dato `sessionId`, apre `EventSource(/sessions/:id/events)`, fa il parse di ogni messaggio e chiama `store.applyEvent`. Gestisce open/error/retry e chiude alla smontatura. Testabile con un mock di `EventSource`.
### 4.4 `widgets/` — registry + renderer
- `registry.ts`: `Record<kind, Renderer>` + `resolve(kind)` con fallback.
- Un file per renderer. Ogni renderer riceve `{descriptor, onRespond(uiResponse)}` e si occupa solo del suo rendering + raccolta risposta. Le opzioni riservate (`back`/`exit`/`other`) e `recommended` sono rese in modo uniforme da un sotto-componente condiviso (`ReservedControls`). Il linkage `option.opens` è gestito da un wrapper che apre il widget figlio e combina le risposte. Invariante no-limbo: nessun renderer può "chiudere senza rispondere" (Esc/cancel ripresenta).
### 4.5 `viewers/` — artefatti (architettura §6)
- `SchemaLinkingViewer`: Mermaid flowchart verticale (default) + tabella gerarchica via toggle (≤45 elementi), commento "perché" inline.
- `SqlViewer`: code block collassabili con header (nome, n° campi, stato test), evidenziazione sintattica via **shiki**, commenti per campo; SQL finale = stesso componente.
- `ResultsPanel`: contestuale — numero in grassetto; lista in **AGGrid** con paginazione (`POST /sql/preview` limit/offset) + export CSV (`POST /sql/export`); selettore `[10 ▾ / tutti]`.
- `MarkdownView`: markdown "mermaid-enhanced" (formattazione + rendering mermaid inline) per testi lunghi, in box scrollabile.
### 4.6 `shell/` — layout a 4 zone (architettura §6)
Nav sinistra (funzioni + lista sessioni) | workflow bar orizzontale (fasi) sopra la chat | chat+input al centro | sidebar destra collassabile con artefatti/COT/thinking. Il widget pendente compare nella zona centrale sotto la chat; gli artefatti referenziati aprono i viewer nella sidebar destra.
---
## 5. Gestione errori ed edge case
- **Kind widget sconosciuto** → fallback renderer (FE-4), il revisore risponde manualmente; nessun crash.
- **SSE disconnesso** → `useSessionStream` ritenta; alla riconnessione il widget pendente viene ri-emesso dal backend (BE-3). I `text_delta` persi nel buco non si recuperano (accettabile: conta l'artefatto/widget finale).
- **Backend non raggiungibile** → stato di connessione visibile (banner), retry sulle query REST (TanStack Query).
- **`POST /response` su sessione non attiva** → il backend risponde 404; la UI invita a riprendere la sessione (`POST /resume`).
- **Preview/export SQL in errore** → il backend ritorna errore (stderr/exit); il `ResultsPanel` mostra il messaggio senza rompere la vista.
---
## 6. Test (FE-5)
- **Unit/component (Vitest + RTL):** ogni renderer widget (incl. fallback, reserved controls, linkage, no-limbo); `sessionStore.applyEvent` per ogni tipo di evento; `useSessionStream` con mock `EventSource`; i viewer con artefatti di esempio.
- **Integration (MSW):** flusso REST (lista/crea sessione, preview/export) mockato a livello rete; SSE mockato. Verifica del ciclo "ui_request renderizzato → risposta → POST corretto".
- **E2E (Playwright):** un test del loop F1 contro il backend reale avviato col `fake-pi-rpc` (`harness/tests/fake_pi/`): crea sessione → riceve il widget F1 via SSE → risponde → avanza. Non in CI deterministica se richiede orchestrazione; documentato come l'analogo dell'e2e del backend.
---
## 7. Strategia di implementazione (slice verticali, FE-6)
1. **Scaffold** Vite+React+TS + Tailwind/ShadCn + QueryClient provider; health check verso il backend.
2. **`api/` + tipi** del contratto widget-descriptor.
3. **`store/` + `stream/`** (SSE) con mock `EventSource`.
4. **`shell/`** a 4 zone (statica, dati finti).
5. **Widget registry + `select`/`info`/`freetext`** → **chiude il loop F1 end-to-end** (crea sessione → riceve widget → risponde).
6. **`multiselect` + `artifact-gate` + `artifact`** + linkage + no-limbo.
7. **Viewer**: schema-linking (mermaid+tabella), poi CTE/SQL (shiki), poi risultati (AGGrid).
8. **Steering, riconnessione, resume, lista/creazione sessioni** complete; selezione model/thinking/provider (da `GET /models`) e workspace (da `GET /workspaces`) alla creazione.
9. **Playwright e2e F1.**
---
## 8. Fuori scope (MVP)
- Auth con header nel browser (MVP=`none`; SSE via `fetch` quando servirà — FE-3).
- UI completa del datamart (azione separata con conferma di pseudoanonimizzazione; la generazione è lato harness).
- Temi multipli, configurabilità del layout, supporto mobile.
- Web app centrale multi-utente (modello A dell'architettura): i contratti non cambiano, è un riposizionamento di deployment.
---
## 9. Rischi aperti
- **Fedeltà del mock SSE/REST vs backend reale.** Mitigazione: i tipi in `api/types.ts` derivano dal contratto reale; il Playwright e2e contro backend+fake-pi cattura le derive.
- **Rendering Mermaid/shiki nel browser** (peso bundle, performance su artefatti grandi). Mitigazione: vincolo ≤45 elementi (schema-linking), lazy-load dei viewer pesanti.
- **`GET /models` lato backend torna `[]` di default** (seam non ancora cablato): la tendina model potrebbe essere vuota finché il backend non espone i modelli — il FE deve degradare con grazia (campo libero / default).
---
## 10. Nota sul piano di implementazione
Questo documento è il design del frontend. La fase di `writing-plans` produrrà il piano di implementazione (uno, come da architettura §11), costruito a slice (vedi §7) con F1 come primo loop chiuso e testato contro il contratto del backend (MSW/mock) e, per l'e2e, contro backend+fake-pi.