Core implementation: - chat_app.py: Chainlit entry point with @cl.on_chat_start, @cl.on_message (streaming via llm.stream_complete), @cl.on_chat_end Reuses Config, KnowledgeBase, ConversationStore, AdminNotifier from src/ Handles registration completion, post-completion updates, new-child flow - src/llm.py: add stream_complete() generator (litellm stream=True) alongside existing complete(); tests added in tests/test_llm.py - src/agent/response_parser.py: extract parse_llm_response(), apply_updates(), fallback_message() from EmailAgent into shared module EmailAgent now delegates to these functions (no logic change) Chainlit configuration: - chainlit.toml: telemetry off, German default, custom CSS + JS paths - chainlit.md: German welcome page with playgroup info Accessibility (WCAG 2.1 AA): - public/custom.css: contrast overrides (≥4.5:1), prefers-reduced-motion (static "…" replaces animated dots), skip link styles, 100dvh fix - public/accessibility.js: MutationObserver injects aria-live="polite" on message list, focus management after agent replies, skip link element Other: - .gitignore: add .chainlit/ (Chainlit runtime, auto-generated) - openspec/config.yaml: populate context field with tech stack - openspec/changes/implement-web-chat/tasks.md: mark completed tasks 95 tests pass. https://claude.ai/code/session_01SUWzMzFvSfWiHXA2p6rPg9
6.3 KiB
6.3 KiB
1. Add Chainlit Dependency
- 1.1 Run
uv add chainlitto add Chainlit topyproject.toml(matches existinguv-based workflow; do not createrequirements.txt) - 1.2 Create
chainlit.tomlat the project root with:name = "Spielgruppe Pumuckl",enable_telemetry = false,custom_css = "/public/custom.css",default_language = "de"
2. Add Streaming Support to src/llm.py
- 2.1 Add a
stream_complete(model: str, system: str, messages: list)generator function tosrc/llm.pythat callslitellm.completion(..., stream=True)and yields text chunks (parallels the existingcomplete()function; both share the sameapi_messagesbuild logic) - 2.2 Add tests for
stream_completeintests/test_llm.py— verify chunks are yielded, empty deltas are skipped, and the model/system/messages args are forwarded correctly
3. Create Chainlit Entry Point
- 3.1 Create
chat_app.pyat the project root with a@cl.on_chat_starthandler that:- Loads
Config.from_env()fromsrc/config.py - Initialises
KnowledgeBase,ConversationStore,AdminNotifierfrom existingsrc/modules - Creates a fresh
ConversationState(use Chainlit's session ID asconversation_id; no email address required at this stage) - Stores state dict in
cl.user_session["state"] - Sends the German welcome message (drawn from
content/sample-responses.md)
- Loads
- 3.2 Add a
@cl.on_messagehandler inchat_app.pythat:- Deserialises
ConversationStatefromcl.user_session["state"] - Appends the parent's message to
state.messagesas aChatMessage(role="user", ...) - Builds the system prompt via
build_system_prompt()fromsrc/agent/prompts.py - Creates a
cl.Message(content="")and streams chunks fromstream_complete()usingmsg.stream_token(chunk), then callsmsg.send() - Parses the completed response text for JSON (reuse
_parse_llm_responselogic fromsrc/agent/core.py— extract intosrc/agent/response_parser.pyif needed) - Applies field updates to
ConversationState.registration; updatesstate.flow_stepandstate.language - Appends the assistant reply to
state.messages - Serialises state back to
cl.user_session["state"] - When
registration_completeis true: saves registration viaConversationStore, sends admin notification viaAdminNotifier, setsstate.completed = True
- Deserialises
- 3.3 Create
chainlit.mdat the project root with the German welcome/intro text shown in the Chainlit sidebar (plain markdown; drawn fromcontent/agent-personality.md)
4. Accessibility CSS
- 4.1 Create
public/directory at the project root - 4.2 Create
public/custom.csswith colour contrast overrides: all normal text ≥ 4.5:1, large text ≥ 3:1, and placeholder text ≥ 4.5:1 — measure Chainlit's default colours with a contrast checker and override as needed - 4.3 Add a
@media (prefers-reduced-motion: reduce)block topublic/custom.cssthat hides the animated typing-dot element and replaces it with a static"…"pseudo-element - 4.4 Add CSS in
public/custom.cssfor the skip link: hidden by default, visible and highlighted on:focus, and targeting#chat-input - 4.5 Add
min-height: 100dvh(dynamic viewport height) to the chat container selector inpublic/custom.cssso the input is not obscured by iOS Safari's virtual keyboard
5. ARIA and Focus Management
- 5.1 Inspect the Chainlit message list container selector in a running browser (dev tools) and add
aria-live="polite"andaria-atomic="false"to it via aMutationObserverJS snippet injected throughchainlit.toml's[UI] custom_jsor aspublic/accessibility.js - 5.2 Extend the JS snippet so that after each completed agent message (after
msg.send(), not during streaming) focus moves to the new message element viaelement.setAttribute("tabindex", "-1"); element.focus() - 5.3 Add the skip link HTML element to the page via the same JS snippet or Chainlit's
[UI] custom_headerconfig so it is the first focusable element
6. Session Management
- 6.1 Test manually: start a conversation in the browser, refresh the page, verify that
cl.user_session["state"]is restored and conversation history is displayed - 6.2 Add a
@cl.on_chat_endhandler inchat_app.pythat logs the session end (no action needed for MVP, but provides a hook for future email-reminder integration) - 6.3 Add a disconnect/reconnect message in Chainlit configuration: "Deine Sitzung ist abgelaufen. Starte ein neues Gespräch oder nutze E-Mail für eine längere Pause." (and English equivalent)
7. Mobile Testing
- 7.1 Test on iOS Safari (device or BrowserStack): tap the text input while virtual keyboard is open — confirm input is not obscured
- 7.2 Test on Android Chrome: tap input, confirm message list scrolls to keep the latest message visible above the keyboard
- 7.3 Test landscape orientation on a mobile screen: confirm no horizontal scroll and layout is usable
8. Accessibility Verification
- 8.1 Run the axe-core browser extension against the running chat page and fix all reported Level A and Level AA violations
- 8.2 Verify keyboard-only flow: Tab → skip link becomes visible → activate → focus moves to message input → type message → Enter → agent replies → focus moves to new message
- 8.3 Test with VoiceOver (macOS or iOS): navigate to input, send a message, confirm agent reply is announced via the live region without manual focus movement
- 8.4 Set OS
prefers-reduced-motion: reduceand confirm the typing indicator shows as static text (not animated) - 8.5 Spot-check contrast of all rendered text elements; document which selectors required overrides in
public/custom.css
9. Final Integration Check
- 9.1 Confirm
chainlit run chat_app.pystarts cleanly with no warnings or import errors - 9.2 Complete an end-to-end registration through the chat interface: verify the registration JSON is saved to
data/registrations/and the admin notification email is sent - 9.3 Confirm Chainlit telemetry is disabled: open the network tab and verify no requests go to Chainlit analytics endpoints
- 9.4 Confirm
ANTHROPIC_API_KEYand other secrets are not printed in logs or error output - 9.5 Update the
contextfield inopenspec/config.yamlwith the finalised tech stack: Python 3.13, Chainlit, LiteLLM, uv, file-based JSON storage