docs(opsx/implement-web-chat): revise tasks to match existing ecosystem

Key corrections from codebase analysis:
- Use `uv add chainlit` (not requirements.txt) — project already uses uv+pyproject.toml
- Remove .env.example task — file already exists with full config
- Add task to extend src/llm.py with stream_complete() using litellm streaming
  instead of calling Anthropic SDK directly
- Reuse existing src/ modules directly in chat_app.py:
  Config, KnowledgeBase, ConversationStore, AdminNotifier, prompts.py
- chat_app.py lives at project root (parallel to main.py), not in a new app/ dir
- Parse LLM response with existing _parse_llm_response logic from core.py
- Session state stored in cl.user_session, keyed by Chainlit session ID

https://claude.ai/code/session_01SUWzMzFvSfWiHXA2p6rPg9
This commit is contained in:
Claude
2026-02-22 07:20:18 +00:00
parent da0e6823f0
commit 08bee013f9
+49 -42
View File
@@ -1,64 +1,71 @@
## 1. Project Setup
## 1. Add Chainlit Dependency
- [ ] 1.1 Create `requirements.txt` with pinned versions: `chainlit`, `anthropic`, `python-dotenv`
- [ ] 1.2 Create `.env.example` documenting required environment variables (`ANTHROPIC_API_KEY`, etc.)
- [ ] 1.3 Create `chainlit.toml` with project name, telemetry disabled, custom CSS path, and default language set to German
- [ ] 1.4 Create the directory structure: `app/`, `app/agent/`, `app/knowledge_base/`, `app/storage/registrations/`, `public/`
- [ ] 1.5 Add `.gitignore` entries for `.env`, `app/storage/registrations/`, `__pycache__/`, `.chainlit/`
- [ ] 1.1 Run `uv add chainlit` to add Chainlit to `pyproject.toml` (matches existing `uv`-based workflow; do not create `requirements.txt`)
- [ ] 1.2 Create `chainlit.toml` at the project root with: `name = "Spielgruppe Pumuckl"`, `enable_telemetry = false`, `custom_css = "/public/custom.css"`, `default_language = "de"`
## 2. Chainlit Application Shell
## 2. Add Streaming Support to src/llm.py
- [ ] 2.1 Create `app/chat.py` with `@cl.on_chat_start` handler that sends the German welcome message from `channel-config.md`
- [ ] 2.2 Add `@cl.on_message` handler that echoes the received message (placeholder — wired to agent in task 4)
- [ ] 2.3 Create `chainlit.md` with the bilingual welcome message (German default, English comment)
- [ ] 2.4 Verify the app starts with `chainlit run app/chat.py` and the welcome message appears in the browser
- [ ] 2.1 Add a `stream_complete(model: str, system: str, messages: list)` generator function to `src/llm.py` that calls `litellm.completion(..., stream=True)` and yields text chunks (parallels the existing `complete()` function; both share the same `api_messages` build logic)
- [ ] 2.2 Add tests for `stream_complete` in `tests/test_llm.py` — verify chunks are yielded, empty deltas are skipped, and the model/system/messages args are forwarded correctly
## 3. Accessibility CSS
## 3. Create Chainlit Entry Point
- [ ] 3.1 Create `public/custom.css` with colour contrast overrides (all normal text ≥ 4.5:1, large text ≥ 3:1, placeholder ≥ 4.5:1) — measure against Chainlit's default colours with a contrast checker
- [ ] 3.2 Add `@media (prefers-reduced-motion: reduce)` block to `custom.css` that hides animated typing dots and replaces with a static `"…"` pseudo-element
- [ ] 3.3 Add a visually-hidden skip link as the first element in the page via Chainlit's `custom_css` or `head` injection, targeting the message input (`#chat-input`)
- [ ] 3.4 Add CSS to make the skip link visible on `:focus`
- [ ] 3.5 Use `dvh` (dynamic viewport height) units in `custom.css` for the chat container height to prevent iOS Safari virtual keyboard from obscuring the input
- [ ] 3.1 Create `chat_app.py` at the project root with a `@cl.on_chat_start` handler that:
- Loads `Config.from_env()` from `src/config.py`
- Initialises `KnowledgeBase`, `ConversationStore`, `AdminNotifier` from existing `src/` modules
- Creates a fresh `ConversationState` (use Chainlit's session ID as `conversation_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`)
- [ ] 3.2 Add a `@cl.on_message` handler in `chat_app.py` that:
- Deserialises `ConversationState` from `cl.user_session["state"]`
- Appends the parent's message to `state.messages` as a `ChatMessage(role="user", ...)`
- Builds the system prompt via `build_system_prompt()` from `src/agent/prompts.py`
- Creates a `cl.Message(content="")` and streams chunks from `stream_complete()` using `msg.stream_token(chunk)`, then calls `msg.send()`
- Parses the completed response text for JSON (reuse `_parse_llm_response` logic from `src/agent/core.py` — extract into `src/agent/response_parser.py` if needed)
- Applies field updates to `ConversationState.registration`; updates `state.flow_step` and `state.language`
- Appends the assistant reply to `state.messages`
- Serialises state back to `cl.user_session["state"]`
- When `registration_complete` is true: saves registration via `ConversationStore`, sends admin notification via `AdminNotifier`, sets `state.completed = True`
- [ ] 3.3 Create `chainlit.md` at the project root with the German welcome/intro text shown in the Chainlit sidebar (plain markdown; drawn from `content/agent-personality.md`)
## 4. Agent Core Stub
## 4. Accessibility CSS
- [ ] 4.1 Create `app/agent/core.py` with a `process_message(message: str, session_state: dict) -> str` function that returns a hardcoded placeholder reply (full agent logic is a separate change)
- [ ] 4.2 Wire `app/chat.py`'s `@cl.on_message` to call `process_message` and stream the reply back using `cl.Message.stream_token()`
- [ ] 4.3 Initialise `cl.user_session` in `@cl.on_chat_start` with an empty registration state dict (`{"collected": {}, "history": []}`)
- [ ] 4.4 Pass `cl.user_session.get("state")` into `process_message` and write back any state updates it returns
- [ ] 4.1 Create `public/` directory at the project root
- [ ] 4.2 Create `public/custom.css` with 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 to `public/custom.css` that hides the animated typing-dot element and replaces it with a static `"…"` pseudo-element
- [ ] 4.4 Add CSS in `public/custom.css` for 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 in `public/custom.css` so the input is not obscured by iOS Safari's virtual keyboard
## 5. ARIA and Focus Management
- [ ] 5.1 Identify the Chainlit message list container selector in the rendered DOM (use browser dev tools)
- [ ] 5.2 Add `aria-live="polite"` and `aria-atomic="false"` to the message list container via `custom.css` content injection or a `<script>` in Chainlit's `head` config
- [ ] 5.3 Inject a small JS snippet (via `chainlit.toml` `head` config) that, after each completed agent message, moves focus to the latest `.message` element by setting `tabindex="-1"` and calling `.focus()`
- [ ] 5.4 Ensure mid-stream tokens do NOT trigger the live region announcement — only fire the focus move after `cl.Message.send()` completes, not during `stream_token()` calls
- [ ] 5.1 Inspect the Chainlit message list container selector in a running browser (dev tools) and add `aria-live="polite"` and `aria-atomic="false"` to it via a `MutationObserver` JS snippet injected through `chainlit.toml`'s `[UI] custom_js` or as `public/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 via `element.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_header` config so it is the first focusable element
## 6. Session Management
- [ ] 6.1 Confirm `cl.user_session` persists across a browser page refresh by testing manually: start a conversation, refresh, verify history is shown
- [ ] 6.2 Add an `@cl.on_chat_end` handler that logs session end (and in future will trigger email-channel reminders)
- [ ] 6.3 Add a disconnect message in the Chainlit error/reconnect UI config: "Deine Sitzung ist abgelaufen. Starte ein neues Gespräch oder nutze E-Mail für längere Pausen." (with English equivalent)
- [ ] 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_end` handler in `chat_app.py` that 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 (real device or BrowserStack): open chat, tap input, confirm keyboard does not obscure input
- [ ] 7.2 Test on Android Chrome: open chat, tap input, confirm input stays visible and message list scrolls correctly
- [ ] 7.3 Test landscape orientation on mobile: confirm no horizontal scroll and layout is usable
- [ ] 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 `axe-core` browser extension against the running chat page and fix any Level A or AA violations
- [ ] 8.2 Verify keyboard-only flow: Tab to skip link → activate → Tab to input → type message → Enter → agent replies → focus moves to reply
- [ ] 8.3 Test with VoiceOver (macOS or iOS): open chat, navigate to input, send a message, confirm agent reply is announced without moving focus manually
- [ ] 8.4 Test with `prefers-reduced-motion: reduce` (set in OS accessibility settings): confirm typing indicator shows as static text, not animated
- [ ] 8.5 Verify colour contrast of all text elements using a browser contrast checker; document any elements that needed overrides
- [ ] 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: reduce` and 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 app/chat.py` starts cleanly with no warnings
- [ ] 9.2 Send a message, receive a reply, refresh the page — confirm history is preserved
- [ ] 9.3 Confirm telemetry is disabled (no outgoing requests to Chainlit analytics in network tab)
- [ ] 9.4 Confirm the `ANTHROPIC_API_KEY` env var is not logged or exposed in any output
- [ ] 9.5 Update `openspec/config.yaml` context field with the chosen tech stack (Python, Chainlit, Anthropic SDK)
- [ ] 9.1 Confirm `chainlit run chat_app.py` starts 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_KEY` and other secrets are not printed in logs or error output
- [ ] 9.5 Update the `context` field in `openspec/config.yaml` with the finalised tech stack: Python 3.13, Chainlit, LiteLLM, uv, file-based JSON storage