feat(chat): implement web chat interface with accessibility
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
This commit is contained in:
@@ -1,22 +1,22 @@
|
||||
## 1. Add Chainlit Dependency
|
||||
|
||||
- [ ] 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"`
|
||||
- [x] 1.1 Run `uv add chainlit` to add Chainlit to `pyproject.toml` (matches existing `uv`-based workflow; do not create `requirements.txt`)
|
||||
- [x] 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. Add Streaming Support to src/llm.py
|
||||
|
||||
- [ ] 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
|
||||
- [x] 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)
|
||||
- [x] 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. Create Chainlit Entry Point
|
||||
|
||||
- [ ] 3.1 Create `chat_app.py` at the project root with a `@cl.on_chat_start` handler that:
|
||||
- [x] 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:
|
||||
- [x] 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`
|
||||
@@ -26,27 +26,27 @@
|
||||
- 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`)
|
||||
- [x] 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. Accessibility CSS
|
||||
|
||||
- [ ] 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
|
||||
- [x] 4.1 Create `public/` directory at the project root
|
||||
- [x] 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
|
||||
- [x] 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
|
||||
- [x] 4.4 Add CSS in `public/custom.css` for the skip link: hidden by default, visible and highlighted on `:focus`, and targeting `#chat-input`
|
||||
- [x] 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 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
|
||||
- [x] 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`
|
||||
- [x] 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()`
|
||||
- [x] 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 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)
|
||||
- [x] 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)
|
||||
- [x] 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
|
||||
|
||||
@@ -68,4 +68,4 @@
|
||||
- [ ] 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
|
||||
- [x] 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
|
||||
|
||||
+26
-5
@@ -3,11 +3,32 @@ schema: spec-driven
|
||||
# Project context (optional)
|
||||
# This is shown to AI when creating artifacts.
|
||||
# Add your tech stack, conventions, style guides, domain knowledge, etc.
|
||||
# Example:
|
||||
# context: |
|
||||
# Tech stack: TypeScript, React, Node.js
|
||||
# We use conventional commits
|
||||
# Domain: e-commerce platform
|
||||
context: |
|
||||
Tech stack:
|
||||
- Language: Python 3.13+
|
||||
- Package manager: uv (pyproject.toml — never requirements.txt)
|
||||
- Chat interface: Chainlit 2.x (entry point: chat_app.py)
|
||||
- LLM access: LiteLLM (provider-agnostic; src/llm.py wraps litellm.completion)
|
||||
- Email channel: IMAP/SMTP via stdlib (imaplib, smtplib); entry point: main.py
|
||||
- Storage: file-based JSON (no database); data/ directory
|
||||
- Knowledge base: admin-editable Markdown files in openspec/.../knowledge-base/
|
||||
- Tests: pytest + pytest-mock (tests/ directory)
|
||||
|
||||
Key architectural decisions:
|
||||
- Channel-agnostic agent core: src/agent/ (prompts, response_parser) is shared
|
||||
between email (src/agent/core.py / EmailAgent) and chat (chat_app.py)
|
||||
- LLM streaming: src/llm.stream_complete() yields chunks via litellm stream=True
|
||||
- Session state for chat: cl.user_session (Chainlit server-side, survives page refresh)
|
||||
- Accessibility: public/custom.css (contrast, dvh, reduced-motion) +
|
||||
public/accessibility.js (ARIA live region, focus management, skip link)
|
||||
- Admin notifications routed by playgroup type (indoor→Andrea, outdoor→Barbara, CC Markus)
|
||||
|
||||
Conventions:
|
||||
- German is the default language; agent auto-detects and switches to English
|
||||
- Use informal "du" in German agent responses
|
||||
- All knowledge base content stays in Markdown (non-technical admins can edit it)
|
||||
- Schema validation required before saving any completed registration
|
||||
- Never hardcode fee amounts or contact details in application code
|
||||
|
||||
# Per-artifact rules (optional)
|
||||
# Add custom rules for specific artifacts.
|
||||
|
||||
Reference in New Issue
Block a user