Closes the AI relay chantier opened by ADR-0024 by landing the SPA
side: a floating-launcher + dialog-panel chatbot that consumes the
BFF's POST /api/ai/chat SSE endpoint shipped in #196.
Stargate's POC widget was rebuilt from scratch rather than ported —
its drag UX, missing aria-live, and minimal screen-reader contract
did not meet ADR-0016's WCAG 2.2 AA + targeted AAA bar.
Key UX + a11y decisions:
- No drag. Stargate's mouse-only floating-bubble drag had no
keyboard equivalent; replaced with a fixed bottom-right launcher
+ windowed/fullscreen toggle.
- role=dialog + aria-modal=false (non-modal) so the page stays
operable behind the panel; focus returns to the launcher on
close via a viewChild + effect pair.
- role=log + aria-live=polite + aria-relevant=additions on the
message list, so screen readers announce incoming tokens
without re-reading the full history. Typing-dots animation is
decorative (aria-hidden) and gated on prefers-reduced-motion.
- Every interactive element honours the 44 × 44 touch-target
minimum (launcher, header buttons, send/stop, citation chips).
- Stop button visible during streaming, wired to AbortController →
ChatClient → upstream LLM cancel.
- Citations rendered as inline footnote chips; clicking opens a
side-panel sibling component with source + score + snippet.
- All strings tagged with $localize per ADR-0019; FR + EN catalogue
entries added.
- 5 new icons in the shared registry: maximize-2, message-circle,
minimize-2, send, square, x.
Architecture:
- ChatbotApiService — native fetch (HttpClient buffers streaming)
+ ReadableStream parser + manual CSRF cookie read + AbortSignal.
- SSE parser — pure function with CRLF tolerance, partial-frame
reassembly, and well-formed JSON / passthrough decoding.
- ChatbotService — signal-based state (view / messages / streaming
/ selected citation) with stop() / send() actions; ephemeral
conversation per session reload.
- ChatbotHost component — host of the widget; ChatbotCitationPanel
extracted as a sibling component to stay under
anyComponentStyle budget (raised from 5/6 to 6/8 KB to match
portal-admin's posture).
- Lazy-loaded via @defer (on idle) in app.html so the chatbot's
~27 KB lazy chunk does not weigh on the initial bundle.
Tests: 27 new specs covering the SSE parser (8), API service (5),
state service (10), and host component (12 — launcher / dialog /
empty state / messages / log live region / input / citation panel).