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>
12 KiB
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, linkageoption.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):
- L'utente apre una sessione →
useSessionStreamapre l'SSE. - Arriva
{type:"ui_request", ui_request:{id, widget:"multiselect", ...}}→ store salva il widget pendente. - Il registry renderizza il
multiselect; l'utente seleziona e conferma. POST /sessions/:id/responsecon{ui_response:{id, choices, decision}}.- Il backend instrada a Pi; arrivano nuovi
text_delta/ il prossimoui_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) erecommendedsono rese in modo uniforme da un sotto-componente condiviso (ReservedControls). Il linkageoption.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/previewlimit/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 →
useSessionStreamritenta; alla riconnessione il widget pendente viene ri-emesso dal backend (BE-3). Itext_deltapersi 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 /responsesu 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
ResultsPanelmostra 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.applyEventper ogni tipo di evento;useSessionStreamcon mockEventSource; 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)
- Scaffold Vite+React+TS + Tailwind/ShadCn + QueryClient provider; health check verso il backend.
api/+ tipi del contratto widget-descriptor.store/+stream/(SSE) con mockEventSource.shell/a 4 zone (statica, dati finti).- Widget registry +
select/info/freetext→ chiude il loop F1 end-to-end (crea sessione → riceve widget → risponde). multiselect+artifact-gate+artifact+ linkage + no-limbo.- Viewer: schema-linking (mermaid+tabella), poi CTE/SQL (shiki), poi risultati (AGGrid).
- Steering, riconnessione, resume, lista/creazione sessioni complete; selezione model/thinking/provider (da
GET /models) e workspace (daGET /workspaces) alla creazione. - Playwright e2e F1.
8. Fuori scope (MVP)
- Auth con header nel browser (MVP=
none; SSE viafetchquando 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.tsderivano 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 /modelslato 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.