Skip to content

feat: add max-char-limit support to chat widget and web chat template#3196

Open
ezkemboi wants to merge 7 commits intodimagi:mainfrom
ezkemboi:feat/prevent-oversized-message-payloads-widget
Open

feat: add max-char-limit support to chat widget and web chat template#3196
ezkemboi wants to merge 7 commits intodimagi:mainfrom
ezkemboi:feat/prevent-oversized-message-payloads-widget

Conversation

@ezkemboi
Copy link
Copy Markdown
Contributor

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 new maxCharLimit prop (passed from the Django host page). Alpine.js-style reactive getters messageTooLong / messageNearLimit drive 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--error styles.
  • chat-session-service.ts: Propagates the maxCharLimit value through the service layer.
  • components.d.ts: Autogenerated Stencil type — adds maxCharLimit?: number prop declaration.
  • templates/chatbots/chat/web_chat.html: Passes max-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_limit and 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

  • This PR requires docs/changelog update

Widget change exposing a new maxCharLimit prop on <open-chat-studio-widget>. Changelog entry: the chat widget now enforces a configurable character limit with a live counter.

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>
@ezkemboi
Copy link
Copy Markdown
Contributor Author

This was separated from #3180
Widget Changes will go after backend changes and are separated.

Comment thread components/chat_widget/src/components/ocs-chat/ocs-chat.tsx Outdated
@ezkemboi ezkemboi marked this pull request as draft April 21, 2026 15:06
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

📝 Walkthrough

Walkthrough

This pull request adds message character limit validation to the chat widget. The feature introduces a new maxCharLimit property that, when set, displays a live character counter, prevents message submission when the limit is exceeded, and provides visual feedback through warning and error states. Changes span translation strings, TypeScript type definitions, CSS styling for counter and error indicators, component logic for validation and UI rendering, documentation, error handling improvements in the session service, and conditional template attribute passing.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding max-char-limit support to the chat widget and web chat template, which aligns with the changeset.
Description check ✅ Passed The description covers product description, technical description, and notes changelog requirements with specific implementation details across affected files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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" (and role="status") to the counter, and/or linking the textarea to the counter via aria-describedby so 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} and aria-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-button title only reflects too-long state.

When the button is disabled for other reasons (uploading, typing, empty input), title is undefined. That's fine, but note the title attribute is not reliably announced by screen readers — the composer.messageTooLong reason should also be exposed via aria-describedby/aria-live (see counter comment above) so keyboard/AT users know why send is blocked. No change strictly required here if the counter gets aria-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

📥 Commits

Reviewing files that changed from the base of the PR and between bf3fe20 and 9dab397.

📒 Files selected for processing (7)
  • components/chat_widget/src/assets/translations/en.json
  • components/chat_widget/src/components.d.ts
  • components/chat_widget/src/components/ocs-chat/ocs-chat.css
  • components/chat_widget/src/components/ocs-chat/ocs-chat.tsx
  • components/chat_widget/src/components/ocs-chat/readme.md
  • components/chat_widget/src/services/chat-session-service.ts
  • templates/chatbots/chat/web_chat.html

Comment thread components/chat_widget/src/components.d.ts
Comment thread components/chat_widget/src/components/ocs-chat/ocs-chat.tsx
user-name="{{ request.user.get_full_name }}"
{% endif %}
persistent-session="true"
{% if max_char_limit %}max-char-limit="{{ max_char_limit }}"{% endif %}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 -20

Repository: dimagi/open-chat-studio

Length of output: 769


🏁 Script executed:

# Look for test directories
fd -t d -e '' | grep -iE '(test|spec|tests)' | head -20

Repository: 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 -20

Repository: 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 -20

Repository: 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 py

Repository: dimagi/open-chat-studio

Length of output: 49


🏁 Script executed:

# Look for any test files in chatbots app specifically
fd -i 'test.*\.py' apps/chatbots

Repository: 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 test

Repository: 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 -20

Repository: 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.py

Repository: 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 js

Repository: 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.html

Repository: 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 3

Repository: 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 py

Repository: 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 2

Repository: 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 py

Repository: 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 3

Repository: 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.py

Repository: 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 py

Repository: 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 5

Repository: 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 js

Repository: 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 2

Repository: 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.

Copy link
Copy Markdown
Contributor

@snopoke snopoke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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)

lisa-tarbo pushed a commit to lisa-tarbo/open-chat-studio that referenced this pull request Apr 28, 2026
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>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@ezkemboi
Copy link
Copy Markdown
Contributor Author

ezkemboi commented May 1, 2026

@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)

I am fixing the lint issue on this.

@ezkemboi ezkemboi marked this pull request as ready for review May 5, 2026 06:34
codescene-delta-analysis[bot]

This comment was marked as outdated.

@ezkemboi
Copy link
Copy Markdown
Contributor Author

ezkemboi commented May 5, 2026

Waiting for #3180 before getting merged or reviewed.

codescene-delta-analysis[bot]

This comment was marked as outdated.

Removed the maxCharLimit property from the widget's component type definition.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

@codescene-delta-analysis codescene-delta-analysis Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1815 to +1819
{this.maxCharLimit != null && (
<div class={`char-counter${this.messageTooLong ? ' char-counter-error' : this.messageNearLimit ? ' char-counter-warning' : ''}`}>
{this.messageInput.length} / {this.maxCharLimit}
</div>
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should drop this counter

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this was based on changes before the #3180 feedback was implemented.

@ezkemboi
Copy link
Copy Markdown
Contributor Author

ezkemboi commented May 6, 2026

@snopoke, I will have a look at this in the evening and make necessary updates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants