feat: add max-char-limit support to chat widget and web chat template#3196
feat: add max-char-limit support to chat widget and web chat template#3196ezkemboi wants to merge 7 commits intodimagi:mainfrom
Conversation
Adds character limit enforcement to the OCS chat widget component: - Live character counter with warning/error states - Disabled send button when message exceeds limit - Updated web_chat.html to pass max-char-limit attribute from backend Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
This was separated from #3180 |
📝 WalkthroughWalkthroughThis pull request adds message character limit validation to the chat widget. The feature introduces a new Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
components/chat_widget/src/components/ocs-chat/ocs-chat.tsx (2)
1765-1769: Consider announcing the counter to assistive tech.The live character counter updates silently. Screen-reader users typing near/over the limit won't be notified, and the disabled send button alone won't explain why submission is blocked. Consider adding
aria-live="polite"(androle="status") to the counter, and/or linking the textarea to the counter viaaria-describedbyso the limit state is announced.♿ Suggested change
- {this.maxCharLimit != null && ( - <div class={`char-counter${this.messageTooLong ? ' char-counter-error' : this.messageNearLimit ? ' char-counter-warning' : ''}`}> - {this.messageInput.length} / {this.maxCharLimit} - </div> - )} + {this.maxCharLimit != null && ( + <div + id="ocs-char-counter" + role="status" + aria-live="polite" + class={`char-counter${this.messageTooLong ? ' char-counter-error' : this.messageNearLimit ? ' char-counter-warning' : ''}`} + > + {this.messageInput.length} / {this.maxCharLimit} + </div> + )}And on the textarea (line 1757), add
aria-describedby={this.maxCharLimit != null ? 'ocs-char-counter' : undefined}andaria-invalid={this.messageTooLong || undefined}.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/chat_widget/src/components/ocs-chat/ocs-chat.tsx` around lines 1765 - 1769, Add accessible live region and link it to the textarea: update the character counter element (give it id "ocs-char-counter") to include aria-live="polite" and role="status" so screen readers announce updates, and on the textarea component (referenced where this.maxCharLimit is used) add aria-describedby={this.maxCharLimit != null ? 'ocs-char-counter' : undefined} and aria-invalid={this.messageTooLong || undefined} so the counter is announced and the over-limit state is exposed to assistive tech.
1798-1807: Minor: send-buttontitleonly reflects too-long state.When the button is disabled for other reasons (uploading, typing, empty input),
titleisundefined. That's fine, but note thetitleattribute is not reliably announced by screen readers — thecomposer.messageTooLongreason should also be exposed viaaria-describedby/aria-live(see counter comment above) so keyboard/AT users know why send is blocked. No change strictly required here if the counter getsaria-live.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/chat_widget/src/components/ocs-chat/ocs-chat.tsx` around lines 1798 - 1807, The send-button currently only sets title for the composer.messageTooLong state so other disabled reasons (uploading, typing, empty input) are not conveyed to assistive tech; update the send button (the component rendering the button where title is set) to also reference an aria-describedby ID that points to a live region or descriptive element that reflects composer.messageTooLong and other disable reasons (e.g., uploading, typing, empty input), or ensure the counter element exposing composer.messageTooLong has aria-live so screen readers announce the reason; specifically, add an aria-describedby attribute on the send button pointing to the existing counter/error element (or create a visually-hidden live region) and ensure that element's content is updated when composer.messageTooLong or other disable conditions change so keyboard/AT users are informed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/chat_widget/src/components.d.ts`:
- Line 108: The declaration of the component property versionNumber conflicts
with its `@internal` documentation in ocs-chat.tsx (around the `@Prop`() on the
versionNumber field) — decide whether it should remain part of the public API or
be private; if it should be internal-only, remove the `@Prop`() decorator from the
versionNumber field in ocs-chat.tsx and convert it to an
internal/service-managed property (or use a private class field/getter) and
update any tests and API request usages accordingly, then regenerate the
component types so components.d.ts no longer exposes versionNumber; if it should
stay public, update the JSDoc/comment to explicitly state "for internal
consumers only" and keep the `@Prop`() but regenerate types to reflect the
clarified documentation.
In `@components/chat_widget/src/components/ocs-chat/ocs-chat.tsx`:
- Around line 504-514: Add unit tests for the new character-limit feature in the
OcsChat component: create tests that render OcsChat (or its exported
component/test wrapper) and exercise the message input and send flow for three
cases — (a) when maxCharLimit is unset: assert no character counter is rendered
and sendMessage (triggered via send button or calling sendMessage) succeeds/does
not block; (b) when message length equals maxCharLimit: assert the counter shows
a warning state (e.g., warning class or text), and send remains enabled; (c)
when message length exceeds maxCharLimit: assert the counter shows an error
state, the send button is disabled, and sendMessage does not proceed. Use the
component name OcsChat, the sendMessage handler (or send button/data-testid),
the input change handler (or message input element), and the character counter
element/data-testid to locate DOM nodes; use Jest + React Testing Library
patterns (render, fireEvent/change, getByTestId/queryByText, expect) to
implement assertions.
In `@templates/chatbots/chat/web_chat.html`:
- Line 38: The template uses max_char_limit but neither chatbot_session_view()
nor _chatbot_chat_ui() includes it in the context, so add max_char_limit into
the version_specific_vars dict returned by both functions (ensure the key name
matches the template attribute: max_char_limit) and propagate any existing
config/default logic used for character limits; then add unit tests that render
web_chat.html via both chatbot_session_view and _chatbot_chat_ui to assert the
attribute is present in the rendered HTML and an integration/widget test that
enforces the char limit when sending messages.
---
Nitpick comments:
In `@components/chat_widget/src/components/ocs-chat/ocs-chat.tsx`:
- Around line 1765-1769: Add accessible live region and link it to the textarea:
update the character counter element (give it id "ocs-char-counter") to include
aria-live="polite" and role="status" so screen readers announce updates, and on
the textarea component (referenced where this.maxCharLimit is used) add
aria-describedby={this.maxCharLimit != null ? 'ocs-char-counter' : undefined}
and aria-invalid={this.messageTooLong || undefined} so the counter is announced
and the over-limit state is exposed to assistive tech.
- Around line 1798-1807: The send-button currently only sets title for the
composer.messageTooLong state so other disabled reasons (uploading, typing,
empty input) are not conveyed to assistive tech; update the send button (the
component rendering the button where title is set) to also reference an
aria-describedby ID that points to a live region or descriptive element that
reflects composer.messageTooLong and other disable reasons (e.g., uploading,
typing, empty input), or ensure the counter element exposing
composer.messageTooLong has aria-live so screen readers announce the reason;
specifically, add an aria-describedby attribute on the send button pointing to
the existing counter/error element (or create a visually-hidden live region) and
ensure that element's content is updated when composer.messageTooLong or other
disable conditions change so keyboard/AT users are informed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 443e84e1-e874-4b8f-93e7-deaac8c360eb
📒 Files selected for processing (7)
components/chat_widget/src/assets/translations/en.jsoncomponents/chat_widget/src/components.d.tscomponents/chat_widget/src/components/ocs-chat/ocs-chat.csscomponents/chat_widget/src/components/ocs-chat/ocs-chat.tsxcomponents/chat_widget/src/components/ocs-chat/readme.mdcomponents/chat_widget/src/services/chat-session-service.tstemplates/chatbots/chat/web_chat.html
| user-name="{{ request.user.get_full_name }}" | ||
| {% endif %} | ||
| persistent-session="true" | ||
| {% if max_char_limit %}max-char-limit="{{ max_char_limit }}"{% endif %} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Description: Look for tests covering the new max character limit behavior.
# Expected: Matches in frontend widget tests and/or Django template/view tests.
fd -i '(test|spec)' --extension ts --extension tsx --extension js --extension jsx --extension py \
| xargs -r rg -n -C3 'maxCharLimit|max-char-limit|max_char_limit|messageTooLong|char-counter'Repository: dimagi/open-chat-studio
Length of output: 51
🏁 Script executed:
# First, check overall repository structure
git ls-files | grep -E '(test|spec)' | head -20Repository: dimagi/open-chat-studio
Length of output: 769
🏁 Script executed:
# Look for test directories
fd -t d -e '' | grep -iE '(test|spec|tests)' | head -20Repository: dimagi/open-chat-studio
Length of output: 49
🏁 Script executed:
# Check if there are any test files related to chatbot or chat functionality
fd -i 'test_.*\.py|.*_test\.py' | head -20Repository: dimagi/open-chat-studio
Length of output: 809
🏁 Script executed:
# Search for ChatbotMessage model and any tests that might reference it
rg -l 'ChatbotMessage' --type py | head -20Repository: dimagi/open-chat-studio
Length of output: 49
🏁 Script executed:
# Search for ChatbotMessage in the codebase to understand the model
rg 'class ChatbotMessage' -A 10 --type pyRepository: dimagi/open-chat-studio
Length of output: 49
🏁 Script executed:
# Look for any test files in chatbots app specifically
fd -i 'test.*\.py' apps/chatbotsRepository: dimagi/open-chat-studio
Length of output: 193
🏁 Script executed:
# Search for character limit, max char, or validation tests
rg -i 'max.*char|char.*limit|messageTooLong' --type py | grep -i testRepository: dimagi/open-chat-studio
Length of output: 342
🏁 Script executed:
# Check if there are frontend/widget tests
fd -t f -e spec.ts -o -e spec.tsx -o -e test.ts -o -e test.tsx | head -20Repository: dimagi/open-chat-studio
Length of output: 183
🏁 Script executed:
# Check the chatbot tests to see if any cover character limits
cat apps/chatbots/tests/test_chatbot_views.pyRepository: dimagi/open-chat-studio
Length of output: 20332
🏁 Script executed:
# Search for max_char_limit specifically in the codebase
rg 'max_char_limit|max-char-limit|maxCharLimit' --type py --type ts --type tsx --type jsRepository: dimagi/open-chat-studio
Length of output: 94
🏁 Script executed:
# Check the web_chat.html template to understand the context
cat templates/chatbots/chat/web_chat.htmlRepository: dimagi/open-chat-studio
Length of output: 1980
🏁 Script executed:
# Search for where max_char_limit is set/passed in views
rg 'max_char_limit' --type py -B 3 -A 3Repository: dimagi/open-chat-studio
Length of output: 49
🏁 Script executed:
# Check the ChatbotMessage model for any char limit fields
rg 'class ChatbotMessage' -A 50 --type pyRepository: dimagi/open-chat-studio
Length of output: 49
🏁 Script executed:
# Search for max_char_limit in template context or views related to web_chat
rg 'web_chat|max_char_limit' --type py -B 2 -A 2Repository: dimagi/open-chat-studio
Length of output: 638
🏁 Script executed:
# Get the full context of web_chat.html rendering in views
rg 'web_chat.html' -B 5 -A 10 --type pyRepository: dimagi/open-chat-studio
Length of output: 1793
🏁 Script executed:
# Search for version_specific_vars to understand what's being passed
rg 'version_specific_vars' --type py -B 3 -A 3Repository: dimagi/open-chat-studio
Length of output: 7361
🏁 Script executed:
# Look at the entire chatbots/views.py to find where max_char_limit might be set
head -200 apps/chatbots/views.pyRepository: dimagi/open-chat-studio
Length of output: 8158
🏁 Script executed:
# Check if max_char_limit is being set anywhere in the entire codebase
rg 'max_char_limit' --type pyRepository: dimagi/open-chat-studio
Length of output: 49
🏁 Script executed:
# Check what context variables are actually being passed to the template
grep -n 'version_specific_vars\|web_chat.html' apps/chatbots/views.py -A 5 -B 5Repository: dimagi/open-chat-studio
Length of output: 2381
🏁 Script executed:
# Double-check: is max_char_limit set anywhere through a middleware or context processor?
rg 'max_char_limit' --type py --type ts --type tsx --type jsRepository: dimagi/open-chat-studio
Length of output: 94
🏁 Script executed:
# Check context processors or template setup that might inject it
rg 'context_processor|template' apps/chatbots/views.py -B 2 -A 2Repository: dimagi/open-chat-studio
Length of output: 2977
Backend context variable missing—max_char_limit is never passed to the template.
The template references max_char_limit at line 38, but neither chatbot_session_view() nor _chatbot_chat_ui() includes it in the context variables passed to web_chat.html. The widget attribute will never render because the variable is undefined. Add max_char_limit to version_specific_vars in both views, and add tests covering both the template rendering and widget enforcement of the limit.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@templates/chatbots/chat/web_chat.html` at line 38, The template uses
max_char_limit but neither chatbot_session_view() nor _chatbot_chat_ui()
includes it in the context, so add max_char_limit into the version_specific_vars
dict returned by both functions (ensure the key name matches the template
attribute: max_char_limit) and propagate any existing config/default logic used
for character limits; then add unit tests that render web_chat.html via both
chatbot_session_view and _chatbot_chat_ui to assert the attribute is present in
the rendered HTML and an integration/widget test that enforces the char limit
when sending messages.
There was a problem hiding this comment.
@ezkemboi there are a lot of formatting changes. Are these coming from your IDE? I don't see them when I run npm run lint. I'm not opposed to them but they should be separated from functional changes if possible otherwise it's hard for reviewers to distinguish what is a formatting change and what is a functional change.
I do notice that we have a prettier config file but it isn't hooked up. I'll hook this up and run it to apply the formatting changes in a separate PR which you can then merge into yours. (now merged: #3234)
Adds an explicit prettier dependency, npm scripts (`format`, `format:check`), a `.prettierignore` for Stencil-generated files, and a repo-wide pre-commit hook scoped to the chat widget. Also renames the deprecated `jsxBracketSameLine` key in `.prettierrc.json` to `bracketSameLine`. Without enforcement, contributors' IDEs were silently auto-formatting on save against the existing `.prettierrc.json`, producing large unrelated diffs in otherwise small PRs (e.g. dimagi#3196). This makes formatting deterministic across contributors and CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d-message-payloads-widget
I am fixing the lint issue on this. |
|
Waiting for #3180 before getting merged or reviewed. |
Removed maxCharLimit property and resolved merge conflicts.
Removed the maxCharLimit property from the widget's component type definition.
There was a problem hiding this comment.
Gates Failed
Prevent hotspot decline
(1 hotspot with Complex Method)
Enforce advisory code health rules
(1 file with Complex Method)
Gates Passed
2 Quality Gates Passed
See analysis details in CodeScene
Reason for failure
| Prevent hotspot decline | Violations | Code Health Impact | |
|---|---|---|---|
| ocs-chat.tsx | 1 rule in this hotspot | 6.23 → 6.19 | Suppress |
| Enforce advisory code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| ocs-chat.tsx | 1 advisory rule | 6.23 → 6.19 | Suppress |
Quality Gate Profile: Clean Code Collective
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.
| {this.maxCharLimit != null && ( | ||
| <div class={`char-counter${this.messageTooLong ? ' char-counter-error' : this.messageNearLimit ? ' char-counter-warning' : ''}`}> | ||
| {this.messageInput.length} / {this.maxCharLimit} | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
I think we should drop this counter
There was a problem hiding this comment.
Yes, this was based on changes before the #3180 feedback was implemented.
|
@snopoke, I will have a look at this in the evening and make necessary updates. |
Product Description
Adds a live character counter and send-button guard to the OCS chat widget. Users see a warning as they approach the character limit and are blocked from sending once they exceed it, giving early feedback instead of a backend error.
Technical Description
Part of #3180 (widget changes separated per changelog/docs automation requirements).
Changes are confined to the StencilJS chat widget component (
components/chat_widget/):ocs-chat.tsx: Reads the newmaxCharLimitprop (passed from the Django host page). Alpine.js-style reactive gettersmessageTooLong/messageNearLimitdrive warning/error CSS classes on the counter and disable the send button when the limit is exceeded.ocs-chat.css: Adds.char-counter,.char-counter--warning, and.char-counter--errorstyles.chat-session-service.ts: Propagates themaxCharLimitvalue through the service layer.components.d.ts: Autogenerated Stencil type — addsmaxCharLimit?: numberprop declaration.templates/chatbots/chat/web_chat.html: Passesmax-char-limit="{{ max_char_limit }}"attribute to<open-chat-studio-widget>when the backend provides a limit.assets/translations/en.json: Adds i18n key for the counter label.The backend that computes
max_char_limitand the HTMX input bar changes live in the companion PR #3180.Demo
See PR #3180 for screenshots of the character counter and error state in the web chat UI.
Docs and Changelog
Widget change exposing a new
maxCharLimitprop on<open-chat-studio-widget>. Changelog entry: the chat widget now enforces a configurable character limit with a live counter.