Compare commits

..
Author SHA1 Message Date
Shivam Mishra 648b9c9507 chore: add logs for debug 2025-05-22 10:08:19 +05:30
611fc82847 feat: Add components to show steps in the copilot thinking process (#11530)
This PR adds the components for new Copilot UI
- Added a Header component
- Added a thinking block.
- Update the outline on copilot input

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2025-05-22 10:08:19 +05:30
ffad8458cb feat: Delete a contact from the contacts page (#11529)
# Pull Request Template

## Description


**This PR includes:**

1. Adds the ability to delete a contact from the contacts list accordion
section.
2. Improves the expand/collapse transition for the accordion.


Fixes
[CW-4375](https://linear.app/chatwoot/issue/CW-4375/allow-users-to-delete-a-contact-from-the-contacts-page)

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

### Loom video

https://www.loom.com/share/8c897d24737f40f6b8b29fef76ba18e2?sid=70910b9d-f3db-4d54-8bfc-820db680e537


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Pranav <pranavrajs@gmail.com>
2025-05-22 10:08:19 +05:30
5869a98e6e fix: account email validation during signup (#11307)
- Refactor email validation logic to be a service
- Use the service for both email/pass signup and Google SSO
- fix account email validation during signup
- Use `blocked_domain` setting for both email/pass signup and Google
Sign In [`BLOCKED_DOMAIN` via GlobalConfig]
- add specs for `account_builder`
- add specs for the new service

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2025-05-22 10:08:19 +05:30
dde08b0bd9 feat: Add support for search_conversations in copilot (#11520)
Earlier, we were manually checking if a user was an agent and filtering
their conversations based on inboxes. This logic should have been part
of the conversation permissions service.

This PR moves the check to the right place and updates the logic
accordingly.

Other updates:
- Add support for search_conversations service for copilot.
- Use PermissionFilterService in contacts/conversations, conversations,
copilot search_conversations.

---------

Co-authored-by: Sojan <sojan@pepalo.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2025-05-22 10:08:19 +05:30
Sivin VargheseandShivam Mishra 5a4201ae58 chore: Display Agent Bot token after creation (#11488)
This PR includes:

- Displaying the Agent Bot token after creation
- Updating the avatar icon when an avatar image is not present

Fixes: https://linear.app/chatwoot/issue/CW-4337/agent-bot-token-not-visible
2025-05-22 10:08:19 +05:30
Sivin VargheseandShivam Mishra fc8fe9a8d1 feat: Prevent saving preferences and status when impersonating (#11164)
This PR will prevent saving user preferences and online status when impersonating. Previously, these settings could be updated during impersonation, causing the user to see a different view or UI settings.

Fixes https://linear.app/chatwoot/issue/CW-4163/impersonation-improvements
2025-05-22 10:08:19 +05:30
Muhsin KelothandShivam Mishra 2fe0761fd5 fix: Display message content for CSAT messages in non-widget inboxes (#11528)
We made so many improvements for CSAT via https://github.com/chatwoot/chatwoot/pull/11485. However, we missed showing message content in the dashboard for CSAT URLs created in non-widget inboxes. This PR fixes the issue by ensuring that CSAT-configured messages are passed along with CSAT responses, otherwise defaulting to the translation.
2025-05-22 10:08:19 +05:30
Sivin VargheseandShivam Mishra 9dd2d14655 fix: Status not updating when creating a Linear issue (#11523) 2025-05-22 10:08:19 +05:30
Sojan 98a50d12e0 Bump version to 4.2.0 2025-05-20 00:11:50 -07:00
PranavandGitHub 16371498ba feat: Add a tool to search Linear Issues in copilot (#11518)
This PR adds a tool to search Linear issues. If the integration is
enabled for the account, the tool will return results as expected. Also
introduces support for an `active?` method, which allows third-party
Copilot tools to be conditionally enabled based on the status of the
integration on the account.
2025-05-19 18:00:38 -07:00
PranavandGitHub d657f35a76 feat: Introduce the concept of tool registry within Captain (#11516)
This PR introduces the concept of a tool registry. The implementation is
straightforward: you can define a tool by creating a class with a
function name. The function name gets registered in the registry and can
be referenced during LLM calls. When the LLM invokes a tool using the
registered name, the registry locates and executes the appropriate tool.
If the LLM calls an unregistered tool, the registry returns an error
indicating that the tool is not defined.
2025-05-19 15:26:38 -07:00
Sivin VargheseandGitHub ad41fd90f9 fix: Fix the translation issue on conversation filter reload (#11513)
# Pull Request Template

## Description

This PR fixes the translation inconsistency in the
`<ConversationBasicFilter />` component, where dropdown options would
revert to English after page reload.

**Cause:**
The component used static arrays for chat status and sort options, with
translations initialized only once. After a reload, it showed system
language (English) until the user's locale was fully loaded.

**Solution:**
Replaced static arrays with computed properties to make translations
reactive. This ensures the options automatically update when the locale
changes.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

### Loom video

**Before**

https://www.loom.com/share/eeac97c59b21480b95ef74813d3d7fa9?sid=0481994a-8d35-4c44-87d0-c6c5a77a54fd

**After**

https://www.loom.com/share/c1bdfbdb19ca4e37bda373f0fe12527a?sid=cb5b1d19-272b-48cb-967c-9a82c2a2b028



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2025-05-19 11:18:06 -07:00
PranavandGitHub f8225f851f fix: Add available_name as method in captain_assistant (#11502)
Fixes
https://linear.app/chatwoot/issue/CW-4366/actionviewtemplateerror-undefined-method-available-name-for-an
2025-05-17 10:06:36 -07:00
f0e8cf4ffe chore(i18n): Update translations (#11501)
Co-authored-by: Pranav <pranavrajs@gmail.com>
2025-05-16 20:14:50 -07:00
cbdac45824 feat: Improve Captain interactions, activity messages (#11493)
Show captain messages under the name of the assistant which generated
the message.

- Add support for `Captain::Assistant` sender type
- Add push_event_data for captain_assistants
- Add activity message handler for captain_assistants
- Update UI to show captain messages under the name of the assistant
- Fix the issue where openAI errors when image is sent
- Add support for custom name of the assistant

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2025-05-16 19:27:57 -07:00
Sojan JoseandGitHub a295d5b61d chore: Add development guidelines documentation for AI Agents (#11243)
Added `AGENTS.md` containing development guidelines, build commands,
code style conventions, and project-specific instructions for easier
onboarding and consistent development practices. Additionally symlink
for `CLAUDE.md` and `windsurf`
2025-05-16 19:23:10 -07:00
Sojan JoseandGitHub 84dae4e4a9 fix: Move the check for CaptainAssistant to enterprise (#11500) 2025-05-16 16:59:43 -07:00
Sivin VargheseandGitHub 6dc73841f2 fix: CSAT select label dropdown overflow issue (#11499)
# Pull Request Template

## Description

This PR fixes a UI bug where the "Select Label" dropdown in the CSAT
survey rule overflowed off the screen. The dropdown now stays within the
visible bounds.


## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

### Loom video

https://www.loom.com/share/6a26ba5524de4fcdb2f209a6ec215ba6?sid=7cf1c84c-b167-4237-9571-00af56f7da18


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2025-05-16 16:11:42 -07:00
eba24ce275 chore: Add activity message for conversation resolutions by Captain (#11492)
Currently, when Captain resolves a conversation, the same activity
message used for auto-resolution is shown. This has caused confusion for
users.
With this change, a distinct activity message will be displayed
specifically for resolutions performed by Captain, improving clarity.


Fixes
https://linear.app/chatwoot/issue/CW-4289/incorrect-activity-message-for-conversations-resolved-by-captain-auto#comment-d2991763

Co-authored-by: Pranav <pranav@chatwoot.com>
2025-05-16 11:57:07 -07:00
Sivin VargheseandGitHub 107ad99b7e fix: Change button type for category icon in edit modal to prevent automatic form submission (#11498)
# Pull Request Template

## Description

This PR fixes an issue where updating the icon for a category didn’t
work while editing. This happened because the edit dialog is wrapped in
a form, and the icon button (missing `type="button"`) triggered a form
submission instead of opening the icon picker.

**Solution:**
Set the icon button’s `type` to `"button"` to prevent unintended form
submission.

Fixes
https://linear.app/chatwoot/issue/CW-4350/updating-an-icon-for-the-category-is-not-working

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2025-05-16 10:14:56 -07:00
Sivin VargheseandGitHub 558ce9fd4f fix: Accidental contact creation on country dropdown toggle (#11494) 2025-05-16 16:14:00 +05:30
42548dc24c fix: Issues with custom attributes in conversation sidebar (#11476)
# Pull Request Template

## Description

This PR fixes,

1. Fix display issue where the attribute value `0` was not shown for
attributes of type "number" and instead displayed as `"---"`
2. Fix issue where the copy and delete buttons were not visible when the
attribute value was `0`
3. Fix an issue with updating contact custom attributes in the
conversation sidebar (caused by the camelCase param key mismatch in
`contacts/update`; only reproducible for attributes type "date")
4. Ensure the delete button is shown for checkbox-type attributes, even
when the value is `true` or `false` (previously hidden when the value
was `false`, despite the key being present)

Fixes
[CW-4326](https://linear.app/chatwoot/issue/CW-4326/custom-attribute-with-value-0-not-displayed-correctly-in-chatwoot)
https://github.com/chatwoot/chatwoot/issues/11459

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

### Loom video

https://www.loom.com/share/77257dc09a37452bab7876d1554b8a03?sid=dc5635eb-4fe0-4f39-8da2-036d71649ffc

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2025-05-16 15:22:18 +05:30
d0611cb7f2 feat: Improve CSAT responses (#11485)
# Pull Request Template

## Description

This PR introduces basic customization options for the CSAT survey:

* **Display Type**: Option to use star ratings instead of emojis.
* **Message Text**: Customize the survey message (up to 200 characters).
* **Survey Rules**: Send surveys based on labels — trigger when a
conversation has or doesn't have a specific label.

Fixes
https://linear.app/chatwoot/document/improve-csat-responses-a61cf30e054e

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

### Loom videos

**Website Channel (Widget)**

https://www.loom.com/share/7f47836cde7940ae9d17b7997d060a18?sid=aad2ad0a-140a-4a09-8829-e01fa2e102c5

**Email Channel (Survey link)**

https://www.loom.com/share/e92f4c4c0f73417ba300a25885e093ce?sid=4bb006f0-1c2a-4352-a232-8bf684e3d757

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Pranav <pranavrajs@gmail.com>
2025-05-16 14:18:52 +05:30
Tanmay Deep SharmaandGitHub e9cda40b71 fix: Twilio multiple attachment fix (#11452) 2025-05-16 08:56:37 +05:30
PranavandGitHub bce1f58e86 chore: Update LLM formatter classes to include additional details (#11491)
This PR introduces support for optionally exposing more data during LLM
function calls. This will be useful as we expand Copilot’s capabilities.

Changes included:
- Add support for ArticleLlmFormatter
- Add missing specs for ContactLLMFormatter and ArticleLLMFormatter
- Add additional spec for ConversationLLMFormatter based on config
2025-05-15 17:47:37 -07:00
PranavandGitHub 4f4ef0389b feat: Add support for persistent copilot threads and messages (#11489)
The agents can see the previous conversations with the copilot if needed
with this change. We would have to cleanup the data after a while. For
now, that is not considered.

This PR adds:
- A new model for copilot_threads (intentionally named thread instead of
conversation to avoid confusion), copilot_messages
- Add the controller to fetch previous threads and messages.
2025-05-15 17:37:04 -07:00
a4c7b73888 fix: Manage Twilio SMS channel via inbox API (#11457)
- Currently Chatwoot manages Twilio Inbox creation in a separate
controller , We want to move this to inboxes api so that we have a
unified API for inbox related changes

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2025-05-15 12:28:54 +05:30
Muhsin KelothandGitHub ac884f58f5 fix: Handle Instagram user consent error for first-time message recipients (#11484)
**Issue**
When attempting to send a message to Instagram users who have never
previously messaged the connected Instagram account, we encounter a
"User consent is required to access user profile" error⁠
⁠
**Fix**
This PR implements proper error handling for cases where user consent is
required. We can only access a user's profile after they have initiated
contact with the Instagram account⁠
2025-05-15 12:02:53 +05:30
Sivin VargheseandGitHub 6b767dbe94 fix: Duplicate attachments when navigating back to a conversation (#11472)
# Pull Request Template

## Description

Fixes
[CW-4330](https://linear.app/chatwoot/issue/CW-4330/bug-attachments-in-gallery-view-gets-duplicated-when-you-navigate-back)

**Cause**
When navigating back to a previously viewed conversation, attachments
were being duplicated in the Gallery View. This occurred because the
`SET_ALL_ATTACHMENTS` mutation was appending new attachments to existing
ones `(attachments.push(...data))` instead of replacing them. Each time
a user returned to a conversation, the attachment list would grow,
showing duplicate images.

**Solution**
Updated the `SET_ALL_ATTACHMENTS` mutation to replace the entire
attachments array instead of appending to it. This change ensures
attachments are cleared before adding new ones when returning to a
previously viewed conversation, preventing duplicates.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

### Loom video

https://www.loom.com/share/b2472c07e95843f6879724b19ef89311?sid=eed42238-b4bc-4dbe-8ede-a25a13b4610d

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2025-05-14 15:40:11 -07:00
696611ae63 feat: Add Teleport component to fix RTL/LTR utility classes (#11455)
# Pull Request Template

## Description

This PR introduces a `CustomTeleport` component that wraps Vue's
Teleport to preserve directionality context (ltr / rtl) when teleporting
content outside the app’s root container.

### Problem
Currently, the app sets the text direction (`[dir="ltr"]` or
`[dir="rtl"]`) on a container `div` inside `<body>`, not on `<body>`
itself. When content is teleported directly into `body`, it no longer
inherits the correct direction context. As a result, direction-aware
utility classes like `ltr:pl-2`, `rtl:ml-1`, etc., break because CSS
selectors like `[dir="ltr"] .ltr\:pl-2` no longer match. Identified this
issue when working on this
[PR](https://github.com/chatwoot/chatwoot/pull/11382)

### Solution
The `CustomTeleport` component automatically applies the correct `[dir]`
attribute (`ltr` or `rtl`) on the teleported content's wrapper based on
the current `isRTL` setting from the store. This ensures that
direction-specific Tailwind utility classes continue to work as
expected, even when the content is rendered outside the app root.

## Type of change

- [x] New feature (non-breaking change which adds functionality)


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Pranav <pranavrajs@gmail.com>
2025-05-12 11:49:23 -07:00
Sivin VargheseandGitHub 1b45eb70c9 feat: Add scroll lock on chat list context menu (#11467)
# Pull Request Template

## Description

This PR uses `useScrollLock` from `VueUse` to lock scrolling on the chat
list panel when the context menu is open.

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

### Loom video

https://www.loom.com/share/68a25af2c1b149f8bd06bdbaafb4d82d?sid=0984fde6-ade2-405a-afc9-6d8f99480874


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2025-05-12 11:14:11 -07:00
Tanmay Deep SharmaandGitHub d14505d464 fix: add named volumes for storage, postgres, and redis (#11465)
This PR updates docker-compose.production.yaml to simplify volume
management and eliminate host-path mounts that were causing permission
and startup errors.
2025-05-12 17:32:12 +05:30
Chatwoot BotandGitHub 142bae5fa4 chore: Update translations (#11458) 2025-05-10 19:21:34 -07:00
PranavandGitHub 61c5d751fc chore: Make the table of contents in help center sticky (#11448) 2025-05-09 08:44:44 +05:30
PranavandGitHub a57dfe4478 fix: Allow resource access without filter type in custom_filters API (#11445)
The custom filters API previously required a filter_type attribute, even
when accessing a resource by its ID, which isn’t necessary. This PR
removes that condition.

Fixes https://github.com/chatwoot/chatwoot/issues/11384
2025-05-08 20:12:05 -07:00
PranavandGitHub 27430752b5 feat: Allow agent bots to update custom attributes in accessible conversations (#11447)
Previously, agent bots weren’t allowed to edit custom attributes in
conversations. But with AI, it’s now more feasible to return accurate
and useful attributes. Since there’s no strong reason to block this,
this PR enables bots to update custom attributes.

Fixes https://github.com/chatwoot/chatwoot/issues/11378
2025-05-08 20:11:02 -07:00
Shivam MishraandGitHub 4d684da288 fix: Update the copy for excluding the unattended conversations (#11450) 2025-05-08 20:08:12 -07:00
Vishnu NarayananandGitHub 588e8a4ee7 fix: throttle stalecontacts job (#11430)
- throttle stale contacts job
- process 20% accounts every day
- reduce batch size from 100 to 20
- add delay between jobs
2025-05-08 20:05:08 -07:00
6766b706c1 feat: Add support for typing events in webhooks (#11423)
Added support for typing events in webhooks. Two new events are now
available: `conversation_typing_on` and `conversation_typing_off`.


<img width="746" alt="Screenshot 2025-05-08 at 4 50 24 PM"
src="https://github.com/user-attachments/assets/62da7b38-de0f-42c5-84f4-066e653da331"
/>

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Pranav <pranavrajs@gmail.com>
2025-05-08 17:28:35 -07:00
c73f8aefc5 feat: Allow support for trusted IPs to disable throttling (#11226)
Co-authored-by: Pranav <pranav@chatwoot.com>
2025-05-08 16:10:30 -07:00
Chatwoot BotandGitHub 823c3df27f chore: Update translations (#11432) 2025-05-08 15:45:53 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e86b70b63e chore(deps-dev): Bump vite from 5.4.18 to 5.4.19 (#11446)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite)
from 5.4.18 to 5.4.19.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v5.4.19</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v5.4.19/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v5.4.19/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->5.4.19 (2025-04-30)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19965">#19965</a>,
check static serve file inside sirv (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19966">#19966</a>)
(<a
href="https://github.com/vitejs/vite/commit/766947e7cbf1cdd07df9737394e8c870401b78b0">766947e</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/19965">#19965</a>
<a
href="https://redirect.github.com/vitejs/vite/issues/19966">#19966</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vitejs/vite/commit/80a333a23103ced0442d4463d1191433d90f5e19"><code>80a333a</code></a>
release: v5.4.19</li>
<li><a
href="https://github.com/vitejs/vite/commit/766947e7cbf1cdd07df9737394e8c870401b78b0"><code>766947e</code></a>
fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19965">#19965</a>,
check static serve file inside sirv (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19966">#19966</a>)</li>
<li>See full diff in <a
href="https://github.com/vitejs/vite/commits/v5.4.19/packages/vite">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=5.4.18&new-version=5.4.19)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-08 15:45:11 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
16e0dbb3aa chore(deps): Bump rack from 2.2.13 to 2.2.14 (#11444)
Bumps [rack](https://github.com/rack/rack) from 2.2.13 to 2.2.14.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rack/rack/blob/main/CHANGELOG.md">rack's
changelog</a>.</em></p>
<blockquote>
<h2>[2.2.14] - 2025-05-06</h2>
<h3>Security</h3>
<ul>
<li><a
href="https://github.com/rack/rack/security/advisories/GHSA-gjh7-p2fx-99vx">CVE-2025-46727</a>
Unbounded parameter parsing in <code>Rack::QueryParser</code> can lead
to memory exhaustion.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/rack/rack/commit/d0dcf75706d72d7e874ee31934a97881c1a439ce"><code>d0dcf75</code></a>
Bump patch version.</li>
<li><a
href="https://github.com/rack/rack/commit/fd44bd7259c2df03cdcf0fa33de6fc4f82befc1b"><code>fd44bd7</code></a>
Remove 2nd argument to <code>unescape</code>.</li>
<li><a
href="https://github.com/rack/rack/commit/3f5a4249118d09d199fe480466c8c6717e43b6e3"><code>3f5a424</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/rack/rack/commit/c48e52f7c57e99e1e1bf54c8760d4f082cd1c89d"><code>c48e52f</code></a>
Merge commit from fork</li>
<li>See full diff in <a
href="https://github.com/rack/rack/compare/v2.2.13...v2.2.14">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=rack&package-manager=bundler&previous-version=2.2.13&new-version=2.2.14)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-08 13:09:21 -07:00
Sivin VargheseandGitHub 94c58051f1 fix: Broken outline in account settings page (#11439) 2025-05-08 21:37:29 +05:30
Shivam MishraandGitHub c87b2109a9 feat: allow auto resolve waiting option (#11436) 2025-05-08 12:52:17 +05:30
Vishnu NarayananandGitHub bfddc4da24 chore: use housekeeping queue for remove_stale_contacts job (#11435)
- Use the housekeeping queue for the `remove_stale_contacts` job
- fix specs
2025-05-08 09:39:49 +05:30
Vishnu NarayananandGitHub 199f2fb86c fix: stale contacts job queue (#11434) 2025-05-08 08:43:34 +05:30
Shivam MishraandGitHub 1b23310e4b fix(ux): don't disable input for auto resolve (#11428) 2025-05-07 21:17:24 +05:30
b533980880 feat: Add support for minutes in auto resolve feature (#11269)
### Summary

- Converts conversation auto-resolution duration from days to minutes
for more
granular control
- Updates validation to allow values from 10 minutes (minimum) to 999
days (maximum)
- Implements smart messaging to show appropriate time units in activity
messages

###  Changes

- Created migration to convert existing durations from days to minutes
(x1440)
- Updated conversation resolver to use minutes instead of days
- Added dynamic translation key selection based on duration value
- Updated related specs and documentation
- Added support for displaying durations in days, hours, or minutes
based on value

###  Test plan

- Verify account validation accepts new minute-based ranges
- Confirm existing account settings are correctly migrated
- Test auto-resolution works properly with minute values
- Ensure proper time unit display in activity messages

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2025-05-07 00:36:15 -07:00
Chatwoot BotandGitHub e08436dde5 chore: Update translations (#11425) 2025-05-07 00:34:06 -07:00
Vishnu NarayananandGitHub 968aa39a9e chore: move stalecontacts job to different sidekiq queue (#11427)
- move stale contacts job to `housekeeping` queue below `low`
2025-05-07 00:31:58 -07:00
ea3ef9064b feat: handle rails turbo morphing (#11422)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2025-05-07 10:22:08 +05:30
c8daa6c1fe chore: Update translations (#11413)
Co-authored-by: Pranav <pranav@chatwoot.com>
2025-05-06 13:34:27 -07:00
Muhsin KelothandGitHub 164f4ec90e fix: Undefined local variable or method error for an instance of Instagram MessageText (#11421)
Fixes https://chatwoot-p3.sentry.io/issues/6557025930?project=6382945
2025-05-06 15:49:43 +05:30
945fa5fd16 chore: Update message bubble orientation (#11348)
# Pull Request Template

## Description

This PR includes,

### Changes

1. **Message Orientation**:  
- Updated the bubble orientation of Bot, Agent, and Private Note
messages to align from **left** to **right**.

2. **Activity Message bubble Styling**:  
- Adjusted **padding** and **border-radius** for activity message
bubbles for better alignment and appearance.

| **Before**  | **After** |
| ------------- | ------------- |
| <img width="559" alt="image"
src="https://github.com/user-attachments/assets/18258ae0-0d8e-4447-a005-9b6643b71f81"
/> | <img width="559" alt="image"
src="https://github.com/user-attachments/assets/425785d8-17f9-4629-8301-f19f23aef201"
/> |




---


Fixes
[CW-4263](https://linear.app/chatwoot/issue/CW-4263/v410-messages-history-incoming-and-outgoing-renders-in-one-line),
https://github.com/chatwoot/chatwoot/issues/11340

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

### Loom video

https://www.loom.com/share/117bbb1dda98451883c9bb17f7cf016b?sid=05eae4d4-af11-4a41-a1d6-dc4e7e2cb281


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2025-05-06 13:21:52 +05:30
6ced918549 feat: Update conversation basic filter (#11415)
# Pull Request Template

## Description

This PR updates the basic filter UI for conversations.

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

### Loom video

https://www.loom.com/share/df69a023a39c4dfca2c12b1ee42a0b2e?sid=977e802e-2865-46f1-ae8e-f89ab5eabc2a


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2025-05-06 12:44:23 +05:30
6e6912aa56 chore: Generate test data for bulk insertion (#11229)
Co-authored-by: Vishnu Narayanan <vishnu@chatwoot.com>
Co-authored-by: Sojan <sojan@pepalo.com>
Co-authored-by: tds-1 <tanmay@qoala.id>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2025-05-06 11:13:11 +05:30
PranavandGitHub cbdbf7900e fix: Update the character count for instructions (#11419)
- Increase the Captain instruction size to 20k instead of 2k
- Fix a display issue in the Captain playground and i18n issue in
Captain form.
2025-05-05 19:41:38 -07:00
PranavandGitHub 7cf051aba0 fix: Show campaigns only if the feature is enabled (#11420)
If the feature is disabled (manually or due to plan changes), the
customer cannot disable the existing campaigns. This PR would fix that.

Fixes
https://linear.app/chatwoot/issue/CW-3691/fix-disable-campaigns-on-plan-downgrade
2025-05-05 19:41:28 -07:00
Sivin VargheseandGitHub 6b42305f59 fix: Show agent bot name and avatar correctly in messages (#11394)
This PR fixes an issue where messages from the agent bot were incorrectly displayed as "BOT" with a missing avatar. It now correctly shows the agent bot’s name and avatar URL in the message list.
2025-05-05 16:26:30 -07:00
bfed849d6a fix: Fix missing translations in copilot (#11411)
This PR does the following:

- Missing translations to the i18n file.
- Test the change with Malayalam (ml) translation. 
- Fixes overflow by setting the width to 100%

_Note_: The prompt text is also set for translation. This is
intentional, there is no change in the behaviour.

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2025-05-04 18:06:24 -07:00
Vishnu NarayananandGitHub f3a807c6f0 chore: enable stale contact removal job on chatwoot cloud (#11390)
- enable stale contact/contact inboxes removal job on chatwoot cloud
2025-05-02 13:36:30 +05:30
Chatwoot BotandGitHub 55316ea0a0 chore: Update translations (#11404) 2025-05-01 16:10:29 -07:00
Shivam MishraandGitHub 3630277d00 fix: Update flag from countries.js (#11401) 2025-05-01 09:03:36 -07:00
Chatwoot BotandGitHub 251abd02b0 chore: Update translations (#11400) 2025-04-30 15:34:11 -07:00
PranavandGitHub fb6409508b feat: Allow customizing the responses, flows in Captain (#11385)
- Ability to provide custom instructions to captain

<img width="1107" alt="Screenshot 2025-04-28 at 6 11 43 PM"
src="https://github.com/user-attachments/assets/f94cbccc-b4d8-48fd-b6b9-55524129bc50"
/>
2025-04-29 15:42:15 -07:00
Sojan JoseandGitHub 970e76ace8 feat: API Endpoints to update message status (#11387)
- Added an api endpoint for update message status ( available only for
api inboxes )
- Moved message status management to a service. 
- Handles case where read status arrive before delivered 

fixes: #10314 , #9962
2025-04-29 15:33:11 -07:00
Shivam MishraandGitHub fa4c1fadba feat: use numbers when fetching from the API (#11391) 2025-04-29 18:12:16 +05:30
59bae616cf feat: add UI for contact notes (#11358)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2025-04-29 16:08:20 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sojan Jose
948a118490 chore(deps): Bump net-imap from 0.4.19 to 0.4.20 (#11386)
Bumps [net-imap](https://github.com/ruby/net-imap) from 0.4.19 to
0.4.20.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ruby/net-imap/releases">net-imap's
releases</a>.</em></p>
<blockquote>
<h2>v0.4.20</h2>
<h2>What's Changed</h2>
<p>This release backports two features to prevent unbounded memory use:
the <code>response_handlers</code> keyword argument to
<code>Net::IMAP.new</code> so response handlers can be added before the
server can send any responses (<a
href="https://redirect.github.com/ruby/net-imap/pull/427">ruby/net-imap#427</a>),
and the <code>max_response_size</code> config attribute (<a
href="https://redirect.github.com/ruby/net-imap/pull/445">ruby/net-imap#445</a>).</p>
<blockquote>
<p>[!NOTE]
The default <code>max_response_size</code> is <code>nil</code>
(unlimited), to avoid backward compatibility issues with secure
connections to trusted servers that are well-behaved. It can be
configured more conservatively to guard against untrusted servers (for
example, connecting to user-provided hostnames). <em>It is the
responsibility of <code>net-imap</code> users to configure their client
appropriately for the server they are connecting to.</em></p>
</blockquote>
<h3>Added</h3>
<ul>
<li> Add <code>response_handlers</code> kwarg to
<code>Net::IMAP.new</code> by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/427">ruby/net-imap#427</a>
<ul>
<li>Backports <a
href="https://redirect.github.com/ruby/net-imap/issues/419">#419</a></li>
</ul>
</li>
<li> Limit max_response_size by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/445">ruby/net-imap#445</a>
<ul>
<li>Backports <a
href="https://redirect.github.com/ruby/net-imap/issues/444">#444</a></li>
</ul>
</li>
</ul>
<h3>Documentation</h3>
<ul>
<li>📚 Backport documentation to v0.4 by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/426">ruby/net-imap#426</a>
<ul>
<li>Backports <a
href="https://redirect.github.com/ruby/net-imap/issues/418">#418</a>, <a
href="https://redirect.github.com/ruby/net-imap/issues/420">#420</a>,
documentation only from <a
href="https://redirect.github.com/ruby/net-imap/issues/416">#416</a>,
and <a
href="https://redirect.github.com/ruby/net-imap/issues/424">#424</a></li>
</ul>
</li>
</ul>
<h3>Other Changes</h3>
<ul>
<li>♻️ Update versioned default configs by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/413">ruby/net-imap#413</a>
<ul>
<li>Backports <a
href="https://redirect.github.com/ruby/net-imap/issues/412">#412</a></li>
</ul>
</li>
<li>♻️ Refactor <code>get_response</code> by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/431">ruby/net-imap#431</a>
<ul>
<li>Backports <a
href="https://redirect.github.com/ruby/net-imap/issues/422">#422</a></li>
</ul>
</li>
<li>♻️ Rational config versions by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/430">ruby/net-imap#430</a>
<ul>
<li>Backports <a
href="https://redirect.github.com/ruby/net-imap/issues/429">#429</a></li>
</ul>
</li>
<li>♻️ Extract ResponseReader from get_response by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/434">ruby/net-imap#434</a>
<ul>
<li>Backports <a
href="https://redirect.github.com/ruby/net-imap/issues/433">#433</a></li>
</ul>
</li>
<li>♻️ Refactoring by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/436">ruby/net-imap#436</a>
<ul>
<li>Backports <a
href="https://redirect.github.com/ruby/net-imap/issues/417">#417</a> and
<a
href="https://redirect.github.com/ruby/net-imap/issues/435">#435</a></li>
</ul>
</li>
</ul>
<h3>Miscellaneous</h3>
<ul>
<li> Various test improvements to v0.4 by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/425">ruby/net-imap#425</a>
<ul>
<li>Backports <a
href="https://redirect.github.com/ruby/net-imap/issues/414">#414</a>, <a
href="https://redirect.github.com/ruby/net-imap/issues/415">#415</a>, <a
href="https://redirect.github.com/ruby/net-imap/issues/421">#421</a>,
and <code>assert_pattern</code> from minitest (originally in <a
href="https://redirect.github.com/ruby/net-imap/issues/333">#333</a>)</li>
</ul>
</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ruby/net-imap/compare/v0.4.19...v0.4.20">https://github.com/ruby/net-imap/compare/v0.4.19...v0.4.20</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/ruby/net-imap/commit/5b8f9aece9a06074e396cb127098f30b681faad8"><code>5b8f9ae</code></a>
🔖 Bump version to 0.4.20</li>
<li><a
href="https://github.com/ruby/net-imap/commit/8222a369ce5c700c2df307c8939ea5dcc8b2832d"><code>8222a36</code></a>
🔀 Merge pull request <a
href="https://redirect.github.com/ruby/net-imap/issues/445">#445</a>
from ruby/backport/v0.4-max_response_size</li>
<li><a
href="https://github.com/ruby/net-imap/commit/641c4c451e7c35f72726f7b19342c6de4143977c"><code>641c4c4</code></a>
 Fix backport compatibility with ruby 2.7</li>
<li><a
href="https://github.com/ruby/net-imap/commit/19bea631c31b82798a4047ac3a6737a41054e45a"><code>19bea63</code></a>
 Make max_response_size configurable</li>
<li><a
href="https://github.com/ruby/net-imap/commit/ddcaabd4fecbd1b0d4b3202bc1befed685d06562"><code>ddcaabd</code></a>
 Limit max response size to 512MiB (hard-coded)</li>
<li><a
href="https://github.com/ruby/net-imap/commit/2ca4dbcaa4401a0177c140e220f0a11688603211"><code>2ca4dbc</code></a>
🔀 Merge pull request <a
href="https://redirect.github.com/ruby/net-imap/issues/436">#436</a>
from ruby/backport/v0.4-refactor-config-and-respons...</li>
<li><a
href="https://github.com/ruby/net-imap/commit/9279f8aa1136f4ee4227ed7ce09bfdc20944dc48"><code>9279f8a</code></a>
 Fix backport compatibility with ruby 2.7</li>
<li><a
href="https://github.com/ruby/net-imap/commit/0e27fc1224b18e338d06db9c2f919e2026d5269f"><code>0e27fc1</code></a>
♻️ Save ResponseReader ivars: <a
href="https://github.com/buff"><code>@​buff</code></a> &amp; <a
href="https://github.com/literal"><code>@​literal</code></a>_size</li>
<li><a
href="https://github.com/ruby/net-imap/commit/17064cd2f094ff019a163808d02483b6a892c4bb"><code>17064cd</code></a>
 Fix backport compatibility with ruby 2.7</li>
<li><a
href="https://github.com/ruby/net-imap/commit/e4bb73453c0aae5b5ade9ce6f352914cbfc62028"><code>e4bb734</code></a>
 Fix Config::AttrTypeCoercion for Ractor sharing</li>
<li>Additional commits viewable in <a
href="https://github.com/ruby/net-imap/compare/v0.4.19...v0.4.20">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=net-imap&package-manager=bundler&previous-version=0.4.19&new-version=0.4.20)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2025-04-28 20:49:26 -07:00
Shivam MishraandGitHub 1a2e6dc4ee feat: integrate LeadSquared CRM (#11284) 2025-04-29 09:14:00 +05:30
c63b583f90 chore: improve plan-based feature handling with plan hierarchy (#11335)
- Refactor HandleStripeEventService to better manage features by plan
- Add constants for features available in each plan tier (Startup,
Business, Enterprise)
- Add channel_instagram to Startup plan features
- Improve downgrade handling to properly disable higher-tier features
- Clean up and optimize tests for maintainability
- Add comprehensive test coverage for plan upgrades and downgrades

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2025-04-28 14:13:56 -07:00
Sivin VargheseandGitHub ef6949e32d fix: Hover issue with Linear issue popup (#11376) 2025-04-25 10:14:23 +05:30
Sivin VargheseandGitHub 9194984091 fix: Alignment issue with logo in standard type bubble (#11364) 2025-04-24 21:57:52 +05:30
ecc95478b9 feat: widget opened and closed events (#11240)
Fixes #9272

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2025-04-24 17:01:39 +05:30
Sivin VargheseandGitHub 73f768e0bf fix: Handle slug validation errors in Help Center (#11368)
# Pull Request Template

## Description

This PR fixes an issue with slug validation in the Help Center portal
settings. Previously, users were able to create or update slugs with
invalid characters such as spaces, slashes, and special symbols, which
cause help center to crash.

With this update, slug creation and updates are now properly validated.
Only slugs that match the allowed pattern will be accepted. No spaces,
underscores, slashes, or special characters are allowed.

Examples: **user**, **user-guide**

---

Fixes
https://linear.app/chatwoot/issue/CW-4273/add-validation-for-help-centre-slugs

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

### Loom video

https://www.loom.com/share/a2ca5e2104984f28b29539293ffed33a?sid=e5064cb8-6220-4c43-99da-242c25d32027


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2025-04-24 16:58:05 +05:30
Sojan JoseandGitHub 8d92862ba9 fix: Update pre-commit hook to handle staged deleted files (#11357)
The pre-commit hook was failing when there were staged deleted files
because:
- It was using 'ls' to filter files, which fails when they don't exist
in the filesystem
- Deleted files are still in Git's staging area but not in the
filesystem

Changes made:
1. Added a check to filter files with 'test -f' before passing to
Rubocop
2. Added the same check for staging Rubocop's changes
3. Added '|| true' to prevent errors when no files match the filters

This ensures the pre-commit hook completes successfully even when files
are staged for deletion.
2025-04-24 16:54:53 +05:30
b2250877d4 chore: upgrade utils to 0.0.43 (#11311)
This upgrade includes a fix for the duplicate `CC` and `TO` emails bug

- https://github.com/chatwoot/utils/releases/tag/v0.0.43
- https://github.com/chatwoot/utils/pull/50

Fixes https://github.com/chatwoot/chatwoot/issues/11210

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2025-04-24 14:37:39 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sojan Jose
2d7148931f chore(deps): Bump nokogiri from 1.18.4 to 1.18.8 (#11347)
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.18.4
to 1.18.8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/sparklemotion/nokogiri/releases">nokogiri's
releases</a>.</em></p>
<blockquote>
<h2>v1.18.8 / 2025-04-21</h2>
<h3>Security</h3>
<ul>
<li>[CRuby] Vendored libxml2 is updated to <a
href="https://gitlab.gnome.org/GNOME/libxml2/-/releases/v2.13.8">v2.13.8</a>
to address CVE-2025-32414 and CVE-2025-32415. See <a
href="https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-5w6v-399v-w3cc">GHSA-5w6v-399v-w3cc</a>
for more information.</li>
</ul>
<!-- raw HTML omitted -->

<pre><code>36badd2eb281fca6214a5188e24a34399b15d89730639a068d12931e2adc210e
nokogiri-1.18.8-aarch64-linux-gnu.gem
664e0f9a77a7122a66d6c03abba7641ca610769a4728db55ee1706a0838b78a2
nokogiri-1.18.8-aarch64-linux-musl.gem
483b5b9fb33653f6f05cbe00d09ea315f268f0e707cfc809aa39b62993008212
nokogiri-1.18.8-arm64-darwin.gem
17de01ca3adf9f8e187883ed73c672344d3dbb3c260f88ffa1008e8dc255a28e
nokogiri-1.18.8-arm-linux-gnu.gem
6e6d7e71fc39572bd613a82d528cf54392c3de1ba5ce974f05c832b8187a040b
nokogiri-1.18.8-arm-linux-musl.gem
8c7464875d9ca7f71080c24c0db7bcaa3940e8be3c6fc4bcebccf8b9a0016365
nokogiri-1.18.8.gem
41002596960ff854198a20aaeb34cff0d445406d5ad85ba7ca9c3fd0c8f03de0
nokogiri-1.18.8-java.gem
11ab0f76772c5f2d718fb253fca5b74c6ef7628b72bbf8deba6ab1ffc93344cf
nokogiri-1.18.8-x64-mingw-ucrt.gem
024cdfe7d9ae3466bba6c06f348fb2a8395d9426b66a3c82f1961b907945cc0c
nokogiri-1.18.8-x86_64-darwin.gem
4a747875db873d18a2985ee2c320a6070c4a414ad629da625fbc58d1a20e5ecc
nokogiri-1.18.8-x86_64-linux-gnu.gem
ddd735fba49475a395b9ea793bb6474e3a3125b89960339604d08a5397de1165
nokogiri-1.18.8-x86_64-linux-musl.gem
</code></pre>
<h2>v1.18.7 / 2025-03-31</h2>
<h3>Dependencies</h3>
<ul>
<li>[CRuby] Vendored libxml2 is updated to <a
href="https://gitlab.gnome.org/GNOME/libxml2/-/releases/v2.13.7">v2.13.7</a>,
which is a bugfix release.</li>
</ul>
<!-- raw HTML omitted -->

<pre><code>57a064ab5440814a69a0e040817bd8154adea68a30d2ff2b3aa515a6a06dbb5f
nokogiri-1.18.7-aarch64-linux-gnu.gem
3e442dc5b69376e84288295fe37cbb890a21ad816a7e571e5e9967b3c1e30cd3
nokogiri-1.18.7-aarch64-linux-musl.gem
083abb2e9ed2646860f6b481a981485a658c6064caafaa81bf1cda1bada2e9d5
nokogiri-1.18.7-arm64-darwin.gem
337d9149deb5ae01022dff7c90f97bed81715fd586aacab0c5809ef933994c5e
nokogiri-1.18.7-arm-linux-gnu.gem
97a26edcc975f780a0822aaf7f7d7427c561067c1c9ee56bd3542960f0c28a6e
nokogiri-1.18.7-arm-linux-musl.gem
6b63ff5defe48f30d1d3b3122f65255ca91df2caf5378c6e0482ce73ff46fb31
nokogiri-1.18.7.gem
2cb83666f35619ec59d24d831bf492e49cfe27b112c222330ee929737f42f2eb
nokogiri-1.18.7-java.gem
681148fbc918aa5d54933d8b48aeb9462ab708d23409797ed750af961107f72b
nokogiri-1.18.7-x64-mingw-ucrt.gem
081d1aa517454ba3415304e2ea51fe411d6a3a809490d0c4aa42799cada417b7
nokogiri-1.18.7-x86_64-darwin.gem
3a0bf946eb2defde13d760f869b61bc8b0c18875afdd3cffa96543cfa3a18005
nokogiri-1.18.7-x86_64-linux-gnu.gem
9d83f8ec1fc37a305fa835d7ee61a4f37899e6ccc6dcb05be6645fa9797605af
nokogiri-1.18.7-x86_64-linux-musl.gem
</code></pre>
<h2>v1.18.6 / 2025-03-24</h2>
<h3>Fixed</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md">nokogiri's
changelog</a>.</em></p>
<blockquote>
<h2>v1.18.8 / 2025-04-21</h2>
<h3>Security</h3>
<ul>
<li>[CRuby] Vendored libxml2 is updated to <a
href="https://gitlab.gnome.org/GNOME/libxml2/-/releases/v2.13.8">v2.13.8</a>
to address CVE-2025-32414 and CVE-2025-32415. See <a
href="https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-5w6v-399v-w3cc">GHSA-5w6v-399v-w3cc</a>
for more information.</li>
</ul>
<h2>v1.18.7 / 2025-03-31</h2>
<h3>Dependencies</h3>
<ul>
<li>[CRuby] Vendored libxml2 is updated to <a
href="https://gitlab.gnome.org/GNOME/libxml2/-/releases/v2.13.7">v2.13.7</a>,
which is a bugfix release.</li>
</ul>
<h2>v1.18.6 / 2025-03-24</h2>
<h3>Fixed</h3>
<ul>
<li>[JRuby] In HTML documents, <code>Node#attribute</code> now returns
the correct attribute. This has been broken, and returning
<code>nil</code>, since v1.17.0. (<a
href="https://redirect.github.com/sparklemotion/nokogiri/issues/3487">#3487</a>)
<a
href="https://github.com/flavorjones"><code>@​flavorjones</code></a></li>
</ul>
<h2>v1.18.5 / 2025-03-19</h2>
<h3>Fixed</h3>
<ul>
<li>[JRuby] Update JRuby's XML serialization so it outputs namespaces
exactly like CRuby. (<a
href="https://redirect.github.com/sparklemotion/nokogiri/issues/3455">#3455</a>,
<a
href="https://redirect.github.com/sparklemotion/nokogiri/issues/3456">#3456</a>)
<a
href="https://github.com/johnnyshields"><code>@​johnnyshields</code></a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/9187f4af0dc3cc7ea439bd4957a2fbfab18f6665"><code>9187f4a</code></a>
version bump to v1.18.8</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/1deea041e3f359ccef67abadf19f0d634bf473dd"><code>1deea04</code></a>
dep: libxml2 to v2.13.8 (branch <code>v1.18.x</code>) (<a
href="https://redirect.github.com/sparklemotion/nokogiri/issues/3509">#3509</a>)</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/6457fe639359edda9f8817994bc4935abae3e81e"><code>6457fe6</code></a>
dep: libxml2 to v2.13.8</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/13e8aa4ef52f39d1273d2435bd6bfa98982471ef"><code>13e8aa4</code></a>
version bump to v1.18.7</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/605699d271ee5d7f58ab29c4550fad42b9cc3d69"><code>605699d</code></a>
dep: bump libxml2 to 2.13.7 (v1.18.x backport) (<a
href="https://redirect.github.com/sparklemotion/nokogiri/issues/3495">#3495</a>)</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/804e59038c4318ebe7ba0dc08105cff2030df415"><code>804e590</code></a>
dep: bump libxml2 to 2.13.7</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/52bf15b62d4e576292e21ac948030783333f9e0b"><code>52bf15b</code></a>
dep(dev): drop Rubocop from JRuby deps</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/189769d72627da5f272c7ffcc14cce49d60b9b5a"><code>189769d</code></a>
version bump to v1.18.6</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/de4982f0cc39995570f5d15d753577bede5804dc"><code>de4982f</code></a>
fix(jruby): Node#attribute in HTML documents (v1.18.x) (<a
href="https://redirect.github.com/sparklemotion/nokogiri/issues/3492">#3492</a>)</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/7d95b0f10cf33c505f11fd7d68d7de8943dda2cd"><code>7d95b0f</code></a>
fix(jruby): Node#attribute in HTML documents</li>
<li>Additional commits viewable in <a
href="https://github.com/sparklemotion/nokogiri/compare/v1.18.4...v1.18.8">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=nokogiri&package-manager=bundler&previous-version=1.18.4&new-version=1.18.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2025-04-23 01:27:51 -07:00
957 changed files with 26010 additions and 4078 deletions
+3 -2
View File
@@ -2,7 +2,7 @@
# https://www.chatwoot.com/docs/self-hosted/configuration/environment-variables/#rails-production-variables
# Used to verify the integrity of signed cookies. so ensure a secure value is set
# SECRET_KEY_BASE should be alphanumeric. Avoid special characters or symbols.
# SECRET_KEY_BASE should be alphanumeric. Avoid special characters or symbols.
# Use `rake secret` to generate this variable
SECRET_KEY_BASE=replace_with_lengthy_secure_hex
@@ -216,6 +216,8 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
# ENABLE_RACK_ATTACK=true
# RACK_ATTACK_LIMIT=300
# ENABLE_RACK_ATTACK_WIDGET_API=true
# Comma-separated list of trusted IPs that bypass Rack Attack throttling rules
# RACK_ATTACK_ALLOWED_IPS=127.0.0.1,::1,192.168.0.10
## Running chatwoot as an API only server
## setting this value to true will disable the frontend dashboard endpoints
@@ -257,4 +259,3 @@ AZURE_APP_SECRET=
# Set to true if you want to remove stale contact inboxes
# contact_inboxes with no conversation older than 90 days will be removed
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
+5 -5
View File
@@ -71,9 +71,6 @@ test/cypress/videos/*
/config/master.key
/config/*.enc
#ignore files under .vscode directory
.vscode
.cursor
# yalc for local testing
.yalc
@@ -92,5 +89,8 @@ yarn-debug.log*
# https://vitejs.dev/guide/env-and-mode.html#env-files
*.local
# Claude.ai config file
CLAUDE.md
# TextEditors & AI Agents config files
.vscode
.claude/settings.local.json
.cursor
+3 -3
View File
@@ -4,8 +4,8 @@
# lint js and vue files
npx --no-install lint-staged
# lint only staged ruby files
git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec rubocop --force-exclusion -a
# lint only staged ruby files that still exist (not deleted)
git diff --name-only --cached | xargs -I {} sh -c 'test -f "{}" && echo "{}"' | grep '\.rb$' | xargs -I {} bundle exec rubocop --force-exclusion -a "{}" || true
# stage rubocop changes to files
git diff --name-only --cached | xargs git add
git diff --name-only --cached | xargs -I {} sh -c 'test -f "{}" && git add "{}"' || true
+1
View File
@@ -0,0 +1 @@
../../AGENTS.md
+58
View File
@@ -0,0 +1,58 @@
# Chatwoot Development Guidelines
## Build / Test / Lint
- **Setup**: `bundle install && pnpm install`
- **Run Dev**: `pnpm dev` or `overmind start -f ./Procfile.dev`
- **Lint JS/Vue**: `pnpm eslint` / `pnpm eslint:fix`
- **Lint Ruby**: `bundle exec rubocop -a`
- **Test JS**: `pnpm test` or `pnpm test:watch`
- **Test Ruby**: `bundle exec rspec spec/path/to/file_spec.rb`
- **Single Test**: `bundle exec rspec spec/path/to/file_spec.rb:LINE_NUMBER`
- **Run Project**: `overmind start -f Procfile.dev`
## Code Style
- **Ruby**: Follow RuboCop rules (150 character max line length)
- **Vue/JS**: Use ESLint (Airbnb base + Vue 3 recommended)
- **Vue Components**: Use PascalCase
- **Events**: Use camelCase
- **I18n**: No bare strings in templates; use i18n
- **Error Handling**: Use custom exceptions (`lib/custom_exceptions/`)
- **Models**: Validate presence/uniqueness, add proper indexes
- **Type Safety**: Use PropTypes in Vue, strong params in Rails
- **Naming**: Use clear, descriptive names with consistent casing
- **Vue API**: Always use Composition API with `<script setup>` at the top
## Styling
- **Tailwind Only**:
- Do not write custom CSS
- Do not use scoped CSS
- Do not use inline styles
- Always use Tailwind utility classes
- **Colors**: Refer to `tailwind.config.js` for color definitions
## General Guidelines
- MVP focus: Least code change, happy-path only
- No unnecessary defensive programming
- Break down complex tasks into small, testable units
- Iterate after confirmation
- Avoid writing specs unless explicitly asked
- Remove dead/unreachable/unused code
- Dont write multiple versions or backups for the same logic — pick the best approach and implement it
- Don't reference Claude in commit messages
## Project-Specific
- **Translations**:
- Only update `en.yml` and `en.json`
- Other languages are handled by the community
- Backend i18n → `en.yml`, Frontend i18n → `en.json`
- **Frontend**:
- Use `components-next/` for message bubbles (the rest is being deprecated)
## Ruby Best Practices
- Use compact `module/class` definitions; avoid nested styles
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+6 -6
View File
@@ -485,7 +485,7 @@ GEM
uri
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.4.19)
net-imap (0.4.20)
date
net-protocol
net-pop (0.1.2)
@@ -501,14 +501,14 @@ GEM
newrelic_rpm (9.6.0)
base64
nio4r (2.7.3)
nokogiri (1.18.4)
nokogiri (1.18.8)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.18.4-arm64-darwin)
nokogiri (1.18.8-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.4-x86_64-darwin)
nokogiri (1.18.8-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.4-x86_64-linux-gnu)
nokogiri (1.18.8-x86_64-linux-gnu)
racc (~> 1.4)
oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1)
@@ -567,7 +567,7 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (2.2.13)
rack (2.2.14)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-contrib (2.5.0)
+1 -25
View File
@@ -32,14 +32,7 @@ class AccountBuilder
end
def validate_email
raise InvalidEmail.new({ domain_blocked: domain_blocked }) if domain_blocked?
address = ValidEmail2::Address.new(@email)
if address.valid? && !address.disposable?
true
else
raise InvalidEmail.new({ valid: address.valid?, disposable: address.disposable? })
end
Account::SignUpEmailValidationService.new(@email).perform
end
def validate_user
@@ -81,21 +74,4 @@ class AccountBuilder
@user.confirm if @confirmed
@user.save!
end
def domain_blocked?
domain = @email.split('@').last
blocked_domains.each do |blocked_domain|
return true if domain.match?(blocked_domain)
end
false
end
def blocked_domains
domains = GlobalConfigService.load('BLOCKED_EMAIL_DOMAINS', '')
return [] if domains.blank?
domains.split("\n").map(&:strip)
end
end
@@ -1,3 +1,6 @@
# TODO : Move this to inboxes controller and deprecate this controller
# No need to retain this controller as we could handle everything centrally in inboxes controller
class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts::BaseController
before_action :authorize_request
@@ -12,10 +12,6 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::
Current.account
).perform
# Only allow conversations from inboxes the user has access to
inbox_ids = Current.user.assigned_inboxes.pluck(:id)
conversations = conversations.where(inbox_id: inbox_ids)
@conversations = conversations.order(last_activity_at: :desc).limit(20)
end
end
@@ -163,9 +163,16 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
@contact.custom_attributes
end
def contact_additional_attributes
return @contact.additional_attributes.merge(permitted_params[:additional_attributes]) if permitted_params[:additional_attributes]
@contact.additional_attributes
end
def contact_update_params
# we want the merged custom attributes not the original one
permitted_params.except(:custom_attributes, :avatar_url).merge({ custom_attributes: contact_custom_attributes })
permitted_params.except(:custom_attributes, :avatar_url)
.merge({ custom_attributes: contact_custom_attributes })
.merge({ additional_attributes: contact_additional_attributes })
end
def set_include_contact_inboxes
@@ -1,4 +1,6 @@
class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::Conversations::BaseController
before_action :ensure_api_inbox, only: :update
def index
@messages = message_finder.perform
end
@@ -11,6 +13,11 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
render_could_not_create_error(e.message)
end
def update
Messages::StatusUpdateService.new(message, permitted_params[:status], permitted_params[:external_error]).perform
@message = message
end
def destroy
ActiveRecord::Base.transaction do
message.update!(content: I18n.t('conversations.messages.deleted'), content_type: :text, content_attributes: { deleted: true })
@@ -21,7 +28,9 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
def retry
return if message.blank?
message.update!(status: :sent, content_attributes: {})
service = Messages::StatusUpdateService.new(message, 'sent')
service.perform
message.update!(content_attributes: {})
::SendReplyJob.perform_later(message.id)
rescue StandardError => e
render_could_not_create_error(e.message)
@@ -56,10 +65,16 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
end
def permitted_params
params.permit(:id, :target_language)
params.permit(:id, :target_language, :status, :external_error)
end
def already_translated_content_available?
message.translations.present? && message.translations[permitted_params[:target_language]].present?
end
# API inbox check
def ensure_api_inbox
# Only API inboxes can update messages
render json: { error: 'Message status update is only allowed for API inboxes' }, status: :forbidden unless @conversation.inbox.api?
end
end
@@ -1,6 +1,6 @@
class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseController
before_action :check_authorization
before_action :fetch_custom_filters, except: [:create]
before_action :fetch_custom_filters, only: [:index]
before_action :fetch_custom_filter, only: [:show, :update, :destroy]
DEFAULT_FILTER_TYPE = 'conversation'.freeze
@@ -9,8 +9,8 @@ class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseContro
def show; end
def create
@custom_filter = current_user.custom_filters.create!(
permitted_payload.merge(account_id: Current.account.id)
@custom_filter = Current.account.custom_filters.create!(
permitted_payload.merge(user: Current.user)
)
render json: { error: @custom_filter.errors.messages }, status: :unprocessable_entity and return unless @custom_filter.valid?
end
@@ -27,14 +27,16 @@ class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseContro
private
def fetch_custom_filters
@custom_filters = current_user.custom_filters.where(
account_id: Current.account.id,
@custom_filters = Current.account.custom_filters.where(
user: Current.user,
filter_type: permitted_params[:filter_type] || DEFAULT_FILTER_TYPE
)
end
def fetch_custom_filter
@custom_filter = @custom_filters.find(permitted_params[:id])
@custom_filter = Current.account.custom_filters.where(
user: Current.user
).find(permitted_params[:id])
end
def permitted_payload
@@ -42,7 +42,9 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
end
def update
@inbox.update!(permitted_params.except(:channel))
inbox_params = permitted_params.except(:channel, :csat_config)
inbox_params[:csat_config] = format_csat_config(permitted_params[:csat_config]) if permitted_params[:csat_config].present?
@inbox.update!(inbox_params)
update_inbox_working_hours
update_channel if channel_update_required?
end
@@ -121,10 +123,22 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
@inbox.channel.save!
end
def format_csat_config(config)
{
display_type: config['display_type'] || 'emoji',
message: config['message'] || '',
survey_rules: {
operator: config.dig('survey_rules', 'operator') || 'contains',
values: config.dig('survey_rules', 'values') || []
}
}
end
def inbox_attributes
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
:lock_to_single_conversation, :portal_id, :sender_name_type, :business_name]
:lock_to_single_conversation, :portal_id, :sender_name_type, :business_name,
{ csat_config: [:display_type, :message, { survey_rules: [:operator, { values: [] }] }] }]
end
def permitted_params(channel_attributes = [])
@@ -94,7 +94,8 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
end
def permitted_params
params.permit(:team_id, :project_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, label_ids: [])
params.permit(:team_id, :project_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, :state_id,
label_ids: [])
end
def fetch_hook
@@ -44,8 +44,9 @@ class Api::V1::AccountsController < Api::BaseController
end
def update
@account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
@account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email))
@account.custom_attributes.merge!(custom_attributes_params)
@account.settings.merge!(settings_params)
@account.custom_attributes['onboarding_step'] = 'invite_team' if @account.custom_attributes['onboarding_step'] == 'account_update'
@account.save!
end
@@ -83,13 +84,17 @@ class Api::V1::AccountsController < Api::BaseController
end
def account_params
params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name)
params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :user_full_name)
end
def custom_attributes_params
params.permit(:industry, :company_size, :timezone)
end
def settings_params
params.permit(:auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting)
end
def check_signup_enabled
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
end
@@ -2,10 +2,15 @@ class Api::V1::Widget::CampaignsController < Api::V1::Widget::BaseController
skip_before_action :set_contact
def index
@campaigns = @web_widget
.inbox
.campaigns
.where(enabled: true, account_id: @web_widget.inbox.account_id)
.includes(:sender)
account = @web_widget.inbox.account
@campaigns = if account.feature_enabled?('campaigns')
@web_widget
.inbox
.campaigns
.where(enabled: true, account_id: account.id)
.includes(:sender)
else
[]
end
end
end
@@ -54,7 +54,7 @@ class Api::V2::AccountsController < Api::BaseController
end
def account_params
params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name)
params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :user_full_name)
end
def check_signup_enabled
@@ -1,6 +1,6 @@
module AccessTokenAuthHelper
BOT_ACCESSIBLE_ENDPOINTS = {
'api/v1/accounts/conversations' => %w[toggle_status toggle_priority create update],
'api/v1/accounts/conversations' => %w[toggle_status toggle_priority create update custom_attributes],
'api/v1/accounts/conversations/messages' => ['create'],
'api/v1/accounts/conversations/assignments' => ['create']
}.freeze
@@ -21,7 +21,7 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
def sign_up_user
return redirect_to login_page_url(error: 'no-account-found') unless account_signup_allowed?
return redirect_to login_page_url(error: 'business-account-only') unless validate_business_account?
return redirect_to login_page_url(error: 'business-account-only') unless validate_signup_email_is_business_domain?
create_account_for_user
token = @resource.send(:set_reset_password_token)
@@ -53,9 +53,11 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
).first
end
def validate_business_account?
# return true if the user is a business account, false if it is a gmail account
auth_hash['info']['email'].downcase.exclude?('@gmail.com')
def validate_signup_email_is_business_domain?
# return true if the user is a business account, false if it is a blocked domain account
Account::SignUpEmailValidationService.new(auth_hash['info']['email']).perform
rescue CustomExceptions::Account::InvalidEmail
false
end
def create_account_for_user
@@ -66,3 +66,5 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
# rubocop:enable Rails/I18nLocaleTexts
end
end
SuperAdmin::AccountsController.prepend_mod_with('SuperAdmin::AccountsController')
@@ -24,9 +24,10 @@ class Twilio::CallbackController < ApplicationController
:Body,
:ToCountry,
:FromState,
:MediaUrl0,
:MediaContentType0,
:MessagingServiceSid
*Array.new(10) { |i| :"MediaUrl#{i}" },
*Array.new(10) { |i| :"MediaContentType#{i}" },
:MessagingServiceSid,
:NumMedia
)
end
end
+32 -6
View File
@@ -9,10 +9,17 @@ class AccountDashboard < Administrate::BaseDashboard
# on pages throughout the dashboard.
enterprise_attribute_types = if ChatwootApp.enterprise?
{
limits: Enterprise::AccountLimitsField,
all_features: Enterprise::AccountFeaturesField
attributes = {
limits: AccountLimitsField
}
# Only show manually managed features in Chatwoot Cloud deployment
attributes[:manually_managed_features] = ManuallyManagedFeaturesField if ChatwootApp.chatwoot_cloud?
# Add all_features last so it appears after manually_managed_features
attributes[:all_features] = AccountFeaturesField
attributes
else
{}
end
@@ -46,7 +53,14 @@ class AccountDashboard < Administrate::BaseDashboard
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
enterprise_show_page_attributes = ChatwootApp.enterprise? ? %i[custom_attributes limits all_features] : []
enterprise_show_page_attributes = if ChatwootApp.enterprise?
attrs = %i[custom_attributes limits]
attrs << :manually_managed_features if ChatwootApp.chatwoot_cloud?
attrs << :all_features
attrs
else
[]
end
SHOW_PAGE_ATTRIBUTES = (%i[
id
name
@@ -61,7 +75,14 @@ class AccountDashboard < Administrate::BaseDashboard
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
enterprise_form_attributes = ChatwootApp.enterprise? ? %i[limits all_features] : []
enterprise_form_attributes = if ChatwootApp.enterprise?
attrs = %i[limits]
attrs << :manually_managed_features if ChatwootApp.chatwoot_cloud?
attrs << :all_features
attrs
else
[]
end
FORM_ATTRIBUTES = (%i[
name
locale
@@ -96,6 +117,11 @@ class AccountDashboard < Administrate::BaseDashboard
# to prevent an error from being raised (wrong number of arguments)
# Reference: https://github.com/thoughtbot/administrate/pull/2356/files#diff-4e220b661b88f9a19ac527c50d6f1577ef6ab7b0bed2bfdf048e22e6bfa74a05R204
def permitted_attributes(action)
super + [limits: {}]
attrs = super + [limits: {}]
# Add manually_managed_features to permitted attributes only for Chatwoot Cloud
attrs << { manually_managed_features: [] } if ChatwootApp.chatwoot_cloud?
attrs
end
end
@@ -1,7 +0,0 @@
require 'administrate/field/base'
class Enterprise::AccountFeaturesField < Administrate::Field::Base
def to_s
data
end
end
+4 -1
View File
@@ -88,7 +88,10 @@ class ConversationFinder
def find_conversation_by_inbox
@conversations = current_account.conversations
@conversations = @conversations.where(inbox_id: @inbox_ids) unless params[:inbox_id].blank? && @is_admin
return unless params[:inbox_id]
@conversations = @conversations.where(inbox_id: @inbox_ids)
end
def find_all_conversations
@@ -15,7 +15,7 @@ module SuperAdmin::AccountFeaturesHelper
end
def self.filter_internal_features(features)
return features if GlobalConfig.get_value('DEPLOYMENT_ENV') == 'cloud'
return features if ChatwootApp.chatwoot_cloud?
internal_features = account_features.select { |f| f['chatwoot_internal'] }.pluck('name')
features.except(*internal_features)
@@ -14,6 +14,13 @@ class CaptainAssistant extends ApiClient {
},
});
}
playground({ assistantId, messageContent, messageHistory }) {
return axios.post(`${this.url}/${assistantId}/playground`, {
message_content: messageContent,
message_history: messageHistory,
});
}
}
export default new CaptainAssistant();
@@ -0,0 +1,39 @@
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
title: { type: String, required: true },
isOpen: { type: Boolean, default: false },
});
const isExpanded = ref(props.isOpen);
const toggleAccordion = () => {
isExpanded.value = !isExpanded.value;
};
watch(
() => props.isOpen,
newValue => {
isExpanded.value = newValue;
}
);
</script>
<template>
<div class="border rounded-lg border-n-slate-4">
<button
class="flex items-center justify-between w-full p-4 text-left"
@click="toggleAccordion"
>
<span class="text-sm font-medium text-n-slate-12">{{ title }}</span>
<span
class="w-5 h-5 transition-transform duration-200 i-lucide-chevron-down"
:class="{ 'rotate-180': isExpanded }"
/>
</button>
<div v-if="isExpanded" class="p-4 pt-0">
<slot />
</div>
</div>
</template>
@@ -0,0 +1,65 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useToggle } from '@vueuse/core';
import Button from 'dashboard/components-next/button/Button.vue';
import ConfirmContactDeleteDialog from 'dashboard/components-next/Contacts/ContactsForm/ConfirmContactDeleteDialog.vue';
defineProps({
selectedContact: {
type: Object,
required: true,
},
});
const { t } = useI18n();
const [showDeleteSection, toggleDeleteSection] = useToggle();
const confirmDeleteContactDialogRef = ref(null);
const openConfirmDeleteContactDialog = () => {
confirmDeleteContactDialogRef.value?.dialogRef.open();
};
</script>
<template>
<div class="flex flex-col items-start border-t border-n-strong px-6 py-5">
<Button
:label="t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT')"
sm
link
slate
class="hover:!no-underline text-n-slate-12"
icon="i-lucide-chevron-down"
trailing-icon
@click="toggleDeleteSection()"
/>
<div
class="transition-all duration-300 ease-in-out grid w-full overflow-hidden"
:class="
showDeleteSection
? 'grid-rows-[1fr] opacity-100 mt-2'
: 'grid-rows-[0fr] opacity-0 mt-0'
"
>
<div class="overflow-hidden min-h-0">
<span class="inline-flex text-n-slate-11 text-sm items-center gap-1">
{{ t('CONTACTS_LAYOUT.CARD.DELETE_CONTACT.MESSAGE') }}
<Button
:label="t('CONTACTS_LAYOUT.CARD.DELETE_CONTACT.BUTTON')"
sm
ruby
link
@click="openConfirmDeleteContactDialog()"
/>
</span>
</div>
</div>
</div>
<ConfirmContactDeleteDialog
ref="confirmDeleteContactDialogRef"
:selected-contact="selectedContact"
/>
</template>
@@ -7,6 +7,7 @@ import ContactsForm from 'dashboard/components-next/Contacts/ContactsForm/Contac
import Button from 'dashboard/components-next/button/Button.vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import Flag from 'dashboard/components-next/flag/Flag.vue';
import ContactDeleteSection from 'dashboard/components-next/Contacts/ContactsCard/ContactDeleteSection.vue';
import countries from 'shared/constants/countries';
const props = defineProps({
@@ -149,15 +150,15 @@ const onClickViewDetails = () => emit('showContact', props.id);
/>
<template #after>
<transition
enter-active-class="overflow-hidden transition-all duration-300 ease-out"
leave-active-class="overflow-hidden transition-all duration-300 ease-in"
enter-from-class="overflow-hidden opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-[690px] sm:max-h-[470px] md:max-h-[410px]"
leave-from-class="opacity-100 max-h-[690px] sm:max-h-[470px] md:max-h-[410px]"
leave-to-class="overflow-hidden opacity-0 max-h-0"
<div
class="transition-all duration-500 ease-in-out grid overflow-hidden"
:class="
isExpanded
? 'grid-rows-[1fr] opacity-100'
: 'grid-rows-[0fr] opacity-0'
"
>
<div v-show="isExpanded" class="w-full">
<div class="overflow-hidden">
<div class="flex flex-col gap-6 p-6 border-t border-n-strong">
<ContactsForm
ref="contactsFormRef"
@@ -176,8 +177,14 @@ const onClickViewDetails = () => emit('showContact', props.id);
/>
</div>
</div>
<ContactDeleteSection
:selected-contact="{
id: props.id,
name: props.name,
}"
/>
</div>
</transition>
</div>
</template>
</CardLayout>
</template>
@@ -47,11 +47,7 @@ defineExpose({ dialogRef });
ref="dialogRef"
type="alert"
:title="t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.TITLE')"
:description="
t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.DESCRIPTION', {
contactName: props.selectedContact.name,
})
"
:description="t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.DESCRIPTION')"
:confirm-button-label="t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.CONFIRM')"
@confirm="handleDialogConfirm"
/>
@@ -87,8 +87,10 @@ useKeyboardEvents(keyboardEvents);
<ContactNoteItem
v-for="note in notes"
:key="note.id"
class="mx-6 py-4"
:note="note"
:written-by="getWrittenBy(note)"
allow-delete
@delete="onDelete"
/>
</div>
@@ -1,6 +1,8 @@
<script setup>
import { useTemplateRef, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { dynamicTime } from 'shared/helpers/timeHelper';
import { useToggle } from '@vueuse/core';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import Button from 'dashboard/components-next/button/Button.vue';
@@ -14,39 +16,63 @@ const props = defineProps({
type: String,
required: true,
},
allowDelete: {
type: Boolean,
default: false,
},
collapsible: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['delete']);
const noteContentRef = useTemplateRef('noteContentRef');
const needsCollapse = ref(false);
const [isExpanded, toggleExpanded] = useToggle();
const { t } = useI18n();
const { formatMessage } = useMessageFormatter();
const handleDelete = () => {
emit('delete', props.note.id);
};
onMounted(() => {
if (props.collapsible) {
// Check if content height exceeds approximately 4 lines
// Assuming line height is ~1.625 and font size is ~14px
const threshold = 14 * 1.625 * 4; // ~84px
needsCollapse.value = noteContentRef.value?.clientHeight > threshold;
}
});
</script>
<template>
<div
class="flex flex-col gap-2 py-2 mx-6 border-b border-n-strong group/note"
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5 py-2.5 min-w-0">
<div class="flex flex-col gap-2 border-b border-n-strong group/note">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-1.5 min-w-0">
<Avatar
:name="note?.user?.name || 'Bot'"
:src="note?.user?.thumbnail || '/assets/images/chatwoot_bot.png'"
:src="
note?.user?.name
? note?.user?.thumbnail
: '/assets/images/chatwoot_bot.png'
"
:size="16"
rounded-full
/>
<div class="min-w-0 truncate">
<span class="inline-flex items-center gap-1 text-sm text-n-slate-11">
<span class="font-medium">{{ writtenBy }}</span>
<span class="font-medium text-n-slate-12">{{ writtenBy }}</span>
{{ t('CONTACTS_LAYOUT.SIDEBAR.NOTES.WROTE') }}
<span class="font-medium">{{ dynamicTime(note.createdAt) }}</span>
<span class="font-medium text-n-slate-12">
{{ dynamicTime(note.createdAt) }}
</span>
</span>
</div>
</div>
<Button
v-if="allowDelete"
variant="faded"
color="ruby"
size="xs"
@@ -56,8 +82,28 @@ const handleDelete = () => {
/>
</div>
<p
ref="noteContentRef"
v-dompurify-html="formatMessage(note.content || '')"
class="mb-0 prose-sm prose-p:mb-1 prose-p:mt-0 prose-ul:mb-1 prose-ul:mt-0 text-n-slate-12"
class="mb-0 prose-sm prose-p:text-sm prose-p:leading-relaxed prose-p:mb-1 prose-p:mt-0 prose-ul:mb-1 prose-ul:mt-0 text-n-slate-12"
:class="{
'line-clamp-4': collapsible && !isExpanded && needsCollapse,
}"
/>
<p v-if="collapsible && needsCollapse">
<Button
variant="faded"
color="blue"
size="xs"
:icon="isExpanded ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
@click="() => toggleExpanded()"
>
<template v-if="isExpanded">
{{ t('CONTACTS_LAYOUT.SIDEBAR.NOTES.COLLAPSE') }}
</template>
<template v-else>
{{ t('CONTACTS_LAYOUT.SIDEBAR.NOTES.EXPAND') }}
</template>
</Button>
</p>
</div>
</template>
@@ -200,6 +200,7 @@ defineExpose({ state, isSubmitDisabled });
:label="state.icon"
color="slate"
size="sm"
type="button"
:icon="!state.icon ? 'i-lucide-smile-plus' : ''"
class="!h-[2.4rem] !w-[2.375rem] absolute top-[1.94rem] !outline-none !rounded-[0.438rem] border-0 ltr:left-px rtl:right-px ltr:!rounded-r-none rtl:!rounded-l-none"
@click="isEmojiPickerOpen = !isEmojiPickerOpen"
@@ -7,8 +7,8 @@ import { useStore, useStoreGetters } from 'dashboard/composables/store';
import { uploadFile } from 'dashboard/helper/uploadHelper';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { useVuelidate } from '@vuelidate/core';
import { required, minLength } from '@vuelidate/validators';
import { shouldBeUrl } from 'shared/helpers/Validators';
import { required, minLength, helpers } from '@vuelidate/validators';
import { shouldBeUrl, isValidSlug } from 'shared/helpers/Validators';
import Button from 'dashboard/components-next/button/Button.vue';
import Input from 'dashboard/components-next/input/Input.vue';
@@ -61,7 +61,16 @@ const liveChatWidgets = computed(() => {
const rules = {
name: { required, minLength: minLength(2) },
slug: { required },
slug: {
required: helpers.withMessage(
() => t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.ERROR'),
required
),
isValidSlug: helpers.withMessage(
() => t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.FORMAT_ERROR'),
isValidSlug
),
},
homePageLink: { shouldBeUrl },
};
@@ -71,9 +80,9 @@ const nameError = computed(() =>
v$.value.name.$error ? t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.ERROR') : ''
);
const slugError = computed(() =>
v$.value.slug.$error ? t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.ERROR') : ''
);
const slugError = computed(() => {
return v$.value.slug.$errors[0]?.$message || '';
});
const homePageLinkError = computed(() =>
v$.value.homePageLink.$error
@@ -6,8 +6,9 @@ import { useAlert, useTrack } from 'dashboard/composables';
import { PORTALS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { convertToCategorySlug } from 'dashboard/helper/commons.js';
import { useVuelidate } from '@vuelidate/core';
import { required, minLength } from '@vuelidate/validators';
import { required, minLength, helpers } from '@vuelidate/validators';
import { buildPortalURL } from 'dashboard/helper/portalHelper';
import { isValidSlug } from 'shared/helpers/Validators';
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
import Input from 'dashboard/components-next/input/Input.vue';
@@ -31,7 +32,16 @@ const state = reactive({
const rules = {
name: { required, minLength: minLength(2) },
slug: { required },
slug: {
required: helpers.withMessage(
() => t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.ERROR'),
required
),
isValidSlug: helpers.withMessage(
() => t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.FORMAT_ERROR'),
isValidSlug
),
},
};
const v$ = useVuelidate(rules, state);
@@ -40,9 +50,9 @@ const nameError = computed(() =>
v$.value.name.$error ? t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.ERROR') : ''
);
const slugError = computed(() =>
v$.value.slug.$error ? t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.ERROR') : ''
);
const slugError = computed(() => {
return v$.value.slug.$errors[0]?.$message || '';
});
const isSubmitDisabled = computed(() => v$.value.$invalid);
@@ -131,6 +141,7 @@ defineExpose({ dialogRef });
:message="
nameError || t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.MESSAGE')
"
@blur="v$.name.$touch()"
/>
<Input
id="portal-slug"
@@ -140,6 +151,8 @@ defineExpose({ dialogRef });
:label="t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.LABEL')"
:message-type="slugError ? 'error' : 'info'"
:message="slugError || buildPortalURL(state.slug)"
@input="v$.slug.$touch()"
@blur="v$.slug.$touch()"
/>
</div>
</Dialog>
@@ -0,0 +1,28 @@
<!--
* Preserves RTL/LTR context when teleporting content
* Ensures direction-specific classes (ltr:tailwind-class, rtl:tailwind-class) work correctly
* when content is teleported outside the app's container with [dir] attribute
-->
<script setup>
import { computed } from 'vue';
import { useMapGetter } from 'dashboard/composables/store';
defineProps({
to: {
type: String,
default: 'body',
},
});
const isRTL = useMapGetter('accounts/isRTL');
const contentDirection = computed(() => (isRTL.value ? 'rtl' : 'ltr'));
</script>
<template>
<Teleport :to="to">
<div :dir="contentDirection">
<slot />
</div>
</Teleport>
</template>
@@ -117,7 +117,7 @@ const STYLE_CONFIG = {
'text-n-ruby-11 hover:enabled:bg-n-ruby-9/10 focus-visible:bg-n-ruby-9/10 outline-n-ruby-8',
ghost:
'text-n-ruby-11 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
link: 'text-n-ruby-9 hover:enabled:underline focus-visible:underline outline-transparent',
link: 'text-n-ruby-9 dark:text-n-ruby-11 hover:enabled:underline focus-visible:underline outline-transparent',
},
amber: {
solid:
@@ -2,6 +2,7 @@
import { computed } from 'vue';
import { usePolicy } from 'dashboard/composables/usePolicy';
import Button from 'dashboard/components-next/button/Button.vue';
import BackButton from 'dashboard/components/widgets/BackButton.vue';
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import Policy from 'dashboard/components/policy.vue';
@@ -23,6 +24,10 @@ const props = defineProps({
type: String,
default: '',
},
backUrl: {
type: [String, Object],
default: '',
},
buttonPolicy: {
type: Array,
default: () => [],
@@ -39,6 +44,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
showKnowMore: {
type: Boolean,
default: true,
},
isEmpty: {
type: Boolean,
default: false,
@@ -73,19 +82,23 @@ const handlePageChange = event => {
class="flex items-start lg:items-center justify-between w-full py-6 lg:py-0 lg:h-20 gap-4 lg:gap-2 flex-col lg:flex-row"
>
<div class="flex gap-4 items-center">
<BackButton v-if="backUrl" :to="backUrl" />
<slot name="headerTitle">
<span class="text-xl font-medium text-n-slate-12">
{{ headerTitle }}
</span>
</slot>
<div v-if="!isEmpty" class="flex items-center gap-2">
<div
v-if="!isEmpty && showKnowMore"
class="flex items-center gap-2"
>
<div class="w-0.5 h-4 rounded-2xl bg-n-weak" />
<slot name="knowMore" />
</div>
</div>
<div
v-if="!showPaywall"
v-if="!showPaywall && buttonLabel"
v-on-clickaway="() => emit('close')"
class="relative group/campaign-button"
>
@@ -104,7 +117,7 @@ const handlePageChange = event => {
</div>
</header>
<main class="flex-1 px-6 overflow-y-auto xl:px-0">
<div class="w-full max-w-[60rem] mx-auto py-4">
<div class="w-full max-w-[60rem] h-full mx-auto py-4">
<slot v-if="!showPaywall" name="controls" />
<div
v-if="isFetching"
@@ -76,9 +76,12 @@ const handleAction = ({ action, value }) => {
<template>
<CardLayout>
<div class="flex justify-between w-full gap-1">
<span class="text-base text-n-slate-12 line-clamp-1">
<router-link
:to="{ name: 'captain_assistants_edit', params: { assistantId: id } }"
class="text-base text-n-slate-12 line-clamp-1 hover:underline transition-colors"
>
{{ name }}
</span>
</router-link>
<div class="flex items-center gap-2">
<div
v-on-clickaway="() => toggleDropdown(false)"
@@ -0,0 +1,111 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import NextButton from 'dashboard/components-next/button/Button.vue';
import MessageList from './MessageList.vue';
import CaptainAssistant from 'dashboard/api/captain/assistant';
const { assistantId } = defineProps({
assistantId: {
type: Number,
required: true,
},
});
const { t } = useI18n();
const messages = ref([]);
const newMessage = ref('');
const isLoading = ref(false);
const formatMessagesForApi = () => {
return messages.value.map(message => ({
role: message.sender,
content: message.content,
}));
};
const resetConversation = () => {
messages.value = [];
newMessage.value = '';
};
const sendMessage = async () => {
if (!newMessage.value.trim() || isLoading.value) return;
const userMessage = {
content: newMessage.value,
sender: 'user',
timestamp: new Date().toISOString(),
};
messages.value.push(userMessage);
const currentMessage = newMessage.value;
newMessage.value = '';
try {
isLoading.value = true;
const { data } = await CaptainAssistant.playground({
assistantId,
messageContent: currentMessage,
messageHistory: formatMessagesForApi(),
});
messages.value.push({
content: data.response,
sender: 'assistant',
timestamp: new Date().toISOString(),
});
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error getting assistant response:', error);
} finally {
isLoading.value = false;
}
};
</script>
<template>
<div
class="flex flex-col h-full rounded-lg p-4 border border-n-slate-4 text-n-slate-11"
>
<div class="mb-4">
<div class="flex justify-between items-center mb-1">
<h3 class="text-lg font-medium">
{{ t('CAPTAIN.PLAYGROUND.HEADER') }}
</h3>
<NextButton
ghost
size="small"
icon="i-lucide-rotate-ccw"
@click="resetConversation"
/>
</div>
<p class="text-sm text-n-slate-11">
{{ t('CAPTAIN.PLAYGROUND.DESCRIPTION') }}
</p>
</div>
<MessageList :messages="messages" :is-loading="isLoading" />
<div
class="flex items-center bg-n-solid-1 outline outline-n-container rounded-lg p-3"
>
<input
v-model="newMessage"
class="flex-1 bg-transparent border-none focus:outline-none text-sm mb-0"
:placeholder="t('CAPTAIN.PLAYGROUND.MESSAGE_PLACEHOLDER')"
@keyup.enter="sendMessage"
/>
<NextButton
ghost
size="small"
:disabled="!newMessage.trim()"
icon="i-lucide-send"
@click="sendMessage"
/>
</div>
<p class="text-xs text-n-slate-11 pt-2 text-center">
{{ t('CAPTAIN.PLAYGROUND.CREDIT_NOTE') }}
</p>
</div>
</template>
@@ -0,0 +1,91 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { ref, watch, nextTick } from 'vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
const props = defineProps({
messages: {
type: Array,
required: true,
},
isLoading: {
type: Boolean,
default: false,
},
});
const messageContainer = ref(null);
const { t } = useI18n();
const { formatMessage } = useMessageFormatter();
const isUserMessage = sender => sender === 'user';
const getMessageAlignment = sender =>
isUserMessage(sender) ? 'justify-end' : 'justify-start';
const getMessageDirection = sender =>
isUserMessage(sender) ? 'flex-row-reverse' : 'flex-row';
const getAvatarName = sender =>
isUserMessage(sender)
? t('CAPTAIN.PLAYGROUND.USER')
: t('CAPTAIN.PLAYGROUND.ASSISTANT');
const getMessageStyle = sender =>
isUserMessage(sender)
? 'bg-n-strong text-n-white'
: 'bg-n-solid-iris text-n-slate-12';
const scrollToBottom = async () => {
await nextTick();
if (messageContainer.value) {
messageContainer.value.scrollTop = messageContainer.value.scrollHeight;
}
};
watch(() => props.messages.length, scrollToBottom);
</script>
<template>
<div ref="messageContainer" class="flex-1 overflow-y-auto mb-4 space-y-2">
<div
v-for="(message, index) in messages"
:key="index"
class="flex"
:class="getMessageAlignment(message.sender)"
>
<div
class="flex items-start gap-1.5"
:class="getMessageDirection(message.sender)"
>
<Avatar :name="getAvatarName(message.sender)" rounded-full :size="24" />
<div
class="max-w-[80%] rounded-lg p-3 text-sm"
:class="getMessageStyle(message.sender)"
>
<div class="break-words" v-html="formatMessage(message.content)" />
</div>
</div>
</div>
<div v-if="isLoading" class="flex justify-start">
<div class="flex items-start gap-1.5">
<Avatar :name="getAvatarName('assistant')" rounded-full :size="24" />
<div
class="max-w-sm rounded-lg p-3 text-sm bg-n-solid-iris text-n-slate-12"
>
<div class="flex gap-1">
<div class="w-2 h-2 rounded-full bg-n-iris-10 animate-bounce" />
<div
class="w-2 h-2 rounded-full bg-n-iris-10 animate-bounce [animation-delay:0.2s]"
/>
<div
class="w-2 h-2 rounded-full bg-n-iris-10 animate-bounce [animation-delay:0.4s]"
/>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,309 @@
<script setup>
import { reactive, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useVuelidate } from '@vuelidate/core';
import { required, minLength } from '@vuelidate/validators';
import { useMapGetter } from 'dashboard/composables/store';
import Input from 'dashboard/components-next/input/Input.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import Editor from 'dashboard/components-next/Editor/Editor.vue';
import Accordion from 'dashboard/components-next/Accordion/Accordion.vue';
const props = defineProps({
mode: {
type: String,
required: true,
validator: value => ['edit', 'create'].includes(value),
},
assistant: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(['submit']);
const { t } = useI18n();
const formState = {
uiFlags: useMapGetter('captainAssistants/getUIFlags'),
};
const initialState = {
name: '',
description: '',
productName: '',
welcomeMessage: '',
handoffMessage: '',
resolutionMessage: '',
instructions: '',
features: {
conversationFaqs: false,
memories: false,
},
};
const state = reactive({ ...initialState });
const validationRules = {
name: { required, minLength: minLength(1) },
description: { required, minLength: minLength(1) },
productName: { required, minLength: minLength(1) },
welcomeMessage: { minLength: minLength(1) },
handoffMessage: { minLength: minLength(1) },
resolutionMessage: { minLength: minLength(1) },
instructions: { minLength: minLength(1) },
};
const v$ = useVuelidate(validationRules, state);
const isLoading = computed(() => formState.uiFlags.value.creatingItem);
const getErrorMessage = field => {
return v$.value[field].$error ? v$.value[field].$errors[0].$message : '';
};
const formErrors = computed(() => ({
name: getErrorMessage('name'),
description: getErrorMessage('description'),
productName: getErrorMessage('productName'),
welcomeMessage: getErrorMessage('welcomeMessage'),
handoffMessage: getErrorMessage('handoffMessage'),
resolutionMessage: getErrorMessage('resolutionMessage'),
instructions: getErrorMessage('instructions'),
}));
const updateStateFromAssistant = assistant => {
const { config = {} } = assistant;
state.name = assistant.name;
state.description = assistant.description;
state.productName = config.product_name;
state.welcomeMessage = config.welcome_message;
state.handoffMessage = config.handoff_message;
state.resolutionMessage = config.resolution_message;
state.instructions = config.instructions;
state.features = {
conversationFaqs: config.feature_faq || false,
memories: config.feature_memory || false,
};
};
const handleBasicInfoUpdate = async () => {
const result = await Promise.all([
v$.value.name.$validate(),
v$.value.description.$validate(),
v$.value.productName.$validate(),
]).then(results => results.every(Boolean));
if (!result) return;
const payload = {
name: state.name,
description: state.description,
config: {
...props.assistant.config,
product_name: state.productName,
},
};
emit('submit', payload);
};
const handleSystemMessagesUpdate = async () => {
const result = await Promise.all([
v$.value.welcomeMessage.$validate(),
v$.value.handoffMessage.$validate(),
v$.value.resolutionMessage.$validate(),
]).then(results => results.every(Boolean));
if (!result) return;
const payload = {
config: {
...props.assistant.config,
welcome_message: state.welcomeMessage,
handoff_message: state.handoffMessage,
resolution_message: state.resolutionMessage,
},
};
emit('submit', payload);
};
const handleInstructionsUpdate = async () => {
const result = await v$.value.instructions.$validate();
if (!result) return;
const payload = {
config: {
...props.assistant.config,
instructions: state.instructions,
},
};
emit('submit', payload);
};
const handleFeaturesUpdate = () => {
const payload = {
config: {
...props.assistant.config,
feature_faq: state.features.conversationFaqs,
feature_memory: state.features.memories,
},
};
emit('submit', payload);
};
watch(
() => props.assistant,
newAssistant => {
if (props.mode === 'edit' && newAssistant) {
updateStateFromAssistant(newAssistant);
}
},
{ immediate: true }
);
</script>
<template>
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
<!-- Basic Information Section -->
<Accordion
:title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.BASIC_INFO')"
is-open
>
<div class="flex flex-col gap-4 pt-4">
<Input
v-model="state.name"
:label="t('CAPTAIN.ASSISTANTS.FORM.NAME.LABEL')"
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.NAME.PLACEHOLDER')"
:message="formErrors.name"
:message-type="formErrors.name ? 'error' : 'info'"
/>
<Editor
v-model="state.description"
:label="t('CAPTAIN.ASSISTANTS.FORM.DESCRIPTION.LABEL')"
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.DESCRIPTION.PLACEHOLDER')"
:message="formErrors.description"
:message-type="formErrors.description ? 'error' : 'info'"
/>
<Input
v-model="state.productName"
:label="t('CAPTAIN.ASSISTANTS.FORM.PRODUCT_NAME.LABEL')"
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.PRODUCT_NAME.PLACEHOLDER')"
:message="formErrors.productName"
:message-type="formErrors.productName ? 'error' : 'info'"
/>
<div class="flex justify-end">
<Button
size="small"
:loading="isLoading"
@click="handleBasicInfoUpdate"
>
{{ t('CAPTAIN.ASSISTANTS.FORM.UPDATE') }}
</Button>
</div>
</div>
</Accordion>
<!-- Instructions Section -->
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.INSTRUCTIONS')">
<div class="flex flex-col gap-4 pt-4">
<Editor
v-model="state.instructions"
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.INSTRUCTIONS.PLACEHOLDER')"
:message="formErrors.instructions"
:max-length="20000"
:message-type="formErrors.instructions ? 'error' : 'info'"
/>
<div class="flex justify-end">
<Button
size="small"
:loading="isLoading"
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
@click="handleInstructionsUpdate"
/>
</div>
</div>
</Accordion>
<!-- Greeting Messages Section -->
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.SYSTEM_MESSAGES')">
<div class="flex flex-col gap-4 pt-4">
<Editor
v-model="state.handoffMessage"
:label="t('CAPTAIN.ASSISTANTS.FORM.HANDOFF_MESSAGE.LABEL')"
:placeholder="
t('CAPTAIN.ASSISTANTS.FORM.HANDOFF_MESSAGE.PLACEHOLDER')
"
:message="formErrors.handoffMessage"
:message-type="formErrors.handoffMessage ? 'error' : 'info'"
/>
<Editor
v-model="state.resolutionMessage"
:label="t('CAPTAIN.ASSISTANTS.FORM.RESOLUTION_MESSAGE.LABEL')"
:placeholder="
t('CAPTAIN.ASSISTANTS.FORM.RESOLUTION_MESSAGE.PLACEHOLDER')
"
:message="formErrors.resolutionMessage"
:message-type="formErrors.resolutionMessage ? 'error' : 'info'"
/>
<div class="flex justify-end">
<Button
size="small"
:loading="isLoading"
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
@click="handleSystemMessagesUpdate"
/>
</div>
</div>
</Accordion>
<!-- Features Section -->
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.FEATURES')">
<div class="flex flex-col gap-4 pt-4">
<div class="flex flex-col gap-2">
<label class="text-sm font-medium text-n-slate-12">
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.TITLE') }}
</label>
<div class="flex flex-col gap-2">
<label class="flex items-center gap-2">
<input
v-model="state.features.conversationFaqs"
type="checkbox"
class="form-checkbox"
/>
{{
t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CONVERSATION_FAQS')
}}
</label>
<label class="flex items-center gap-2">
<input
v-model="state.features.memories"
type="checkbox"
class="form-checkbox"
/>
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
</label>
</div>
</div>
<div class="flex justify-end">
<Button
size="small"
:loading="isLoading"
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
@click="handleFeaturesUpdate"
/>
</div>
</div>
</Accordion>
</form>
</template>
@@ -1,5 +1,6 @@
<script setup>
import { nextTick, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useTrack } from 'dashboard/composables';
import { COPILOT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
@@ -39,6 +40,8 @@ const props = defineProps({
const emit = defineEmits(['sendMessage', 'reset', 'setAssistant']);
const { t } = useI18n();
const COPILOT_USER_ROLES = ['assistant', 'system'];
const sendMessage = message => {
@@ -47,7 +50,7 @@ const sendMessage = message => {
};
const useSuggestion = opt => {
emit('sendMessage', opt.prompt);
emit('sendMessage', t(opt.prompt));
useTrack(COPILOT_EVENTS.SEND_SUGGESTED);
};
@@ -66,16 +69,16 @@ const scrollToBottom = async () => {
const promptOptions = [
{
label: 'Summarize this conversation',
prompt: `Summarize the key points discussed between the customer and the support agent, including the customer's concerns, questions, and the solutions or responses provided by the support agent`,
label: 'CAPTAIN.COPILOT.PROMPTS.SUMMARIZE.LABEL',
prompt: 'CAPTAIN.COPILOT.PROMPTS.SUMMARIZE.CONTENT',
},
{
label: 'Suggest an answer',
prompt: `Analyze the customers inquiry, and draft a response that effectively addresses their concerns or questions. Ensure the reply is clear, concise, and provides helpful information.`,
label: 'CAPTAIN.COPILOT.PROMPTS.SUGGEST.LABEL',
prompt: 'CAPTAIN.COPILOT.PROMPTS.SUGGEST.CONTENT',
},
{
label: 'Rate this conversation',
prompt: `Review the conversation to see how well it meets the customers needs. Share a rating out of 5 based on tone, clarity, and effectiveness.`,
label: 'CAPTAIN.COPILOT.PROMPTS.RATE.LABEL',
prompt: 'CAPTAIN.COPILOT.PROMPTS.RATE.CONTENT',
},
];
@@ -89,7 +92,7 @@ watch(
</script>
<template>
<div class="flex flex-col h-full text-sm leading-6 tracking-tight">
<div class="flex flex-col h-full text-sm leading-6 tracking-tight w-full">
<div ref="chatContainer" class="flex-1 px-4 py-4 space-y-6 overflow-y-auto">
<template v-for="message in messages" :key="message.id">
<CopilotAgentMessage
@@ -121,7 +124,7 @@ watch(
class="px-2 py-1 rounded-md border border-n-weak bg-n-slate-2 text-n-slate-11 flex items-center gap-1"
@click="() => useSuggestion(prompt)"
>
<span>{{ prompt.label }}</span>
<span>{{ t(prompt.label) }}</span>
<Icon icon="i-lucide-chevron-right" />
</button>
</div>
@@ -0,0 +1,21 @@
<script setup>
import CopilotHeader from './CopilotHeader.vue';
</script>
<template>
<Story
title="Captain/Copilot/CopilotHeader"
:layout="{ type: 'grid', width: '800px' }"
>
<!-- Default State -->
<Variant title="Default State">
<CopilotHeader />
</Variant>
<!-- With New Conversation Button -->
<Variant title="With New Conversation Button">
<!-- eslint-disable-next-line vue/prefer-true-attribute-shorthand -->
<CopilotHeader :has-messages="true" />
</Variant>
</Story>
</template>
@@ -0,0 +1,32 @@
<script setup>
import Button from '../button/Button.vue';
defineProps({
hasMessages: {
type: Boolean,
default: false,
},
});
defineEmits(['reset', 'close']);
</script>
<template>
<div
class="flex items-center justify-between px-5 py-2 border-b border-n-weak h-12"
>
<div class="flex items-center justify-between gap-2 flex-1">
<span class="font-medium text-sm text-n-slate-12">
{{ $t('CAPTAIN.COPILOT.TITLE') }}
</span>
<div class="flex items-center">
<Button
v-if="hasMessages"
icon="i-lucide-plus"
ghost
sm
@click="$emit('reset')"
/>
<Button icon="i-lucide-x" ghost sm @click="$emit('close')" />
</div>
</div>
</div>
</template>
@@ -13,19 +13,16 @@ const sendMessage = () => {
</script>
<template>
<form
class="border border-n-weak bg-n-alpha-3 rounded-lg h-12 flex"
@submit.prevent="sendMessage"
>
<form class="relative" @submit.prevent="sendMessage">
<input
v-model="message"
type="text"
:placeholder="$t('CAPTAIN.COPILOT.SEND_MESSAGE')"
class="w-full reset-base bg-transparent px-4 py-3 text-n-slate-11 text-sm"
class="w-full reset-base bg-n-alpha-3 ltr:pl-4 ltr:pr-12 rtl:pl-12 rtl:pr-4 py-3 text-n-slate-11 text-sm border border-n-weak rounded-lg focus:outline-none focus:ring-1 focus:ring-n-blue-11 focus:border-n-blue-11"
@keyup.enter="sendMessage"
/>
<button
class="h-auto w-12 flex items-center justify-center text-n-slate-11"
class="absolute ltr:right-1 rtl:left-1 top-1/2 -translate-y-1/2 h-9 w-10 flex items-center justify-center text-n-slate-11 hover:text-n-blue-11"
type="submit"
>
<i class="i-ph-arrow-up" />
@@ -0,0 +1,25 @@
<script setup>
import Icon from '../../components-next/icon/Icon.vue';
defineProps({
content: {
type: String,
required: true,
},
});
</script>
<template>
<div
class="flex flex-col gap-2 p-3 rounded-lg bg-n-background/50 border border-n-weak hover:bg-n-background/80 transition-colors duration-200"
>
<div class="flex items-start gap-2">
<Icon
icon="i-lucide-sparkles"
class="w-4 h-4 mt-0.5 flex-shrink-0 text-n-slate-9"
/>
<div class="text-sm text-n-slate-11">
{{ content }}
</div>
</div>
</div>
</template>
@@ -0,0 +1,34 @@
<script setup>
import CopilotThinkingGroup from './CopilotThinkingGroup.vue';
const messages = [
{
id: 1,
content: 'Analyzing the user query',
reasoning: 'Breaking down the request into actionable steps',
},
{
id: 2,
content: 'Searching codebase',
reasoning: 'Looking for relevant files and functions',
},
{
id: 3,
content: 'Generating response',
reasoning: 'Composing a helpful and accurate answer',
},
];
</script>
<template>
<Story title="Captain/Copilot/CopilotThinkingGroup" group="components">
<Variant title="Default">
<CopilotThinkingGroup :messages="messages" />
</Variant>
<Variant title="With Default Collapsed">
<!-- eslint-disable-next-line -->
<CopilotThinkingGroup :messages="messages" :default-collapsed="true" />
</Variant>
</Story>
</template>
@@ -0,0 +1,61 @@
<script setup>
import { ref, watch, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import Icon from '../icon/Icon.vue';
import CopilotThinkingBlock from './CopilotThinkingBlock.vue';
const props = defineProps({
messages: { type: Array, required: true },
defaultCollapsed: { type: Boolean, default: false },
});
const { t } = useI18n();
const isExpanded = ref(!props.defaultCollapsed);
const thinkingCount = computed(() => props.messages.length);
watch(
() => props.defaultCollapsed,
newValue => {
if (newValue) {
isExpanded.value = false;
}
}
);
</script>
<template>
<div class="flex flex-col gap-2">
<button
class="group flex items-center gap-2 text-xs text-n-slate-10 hover:text-n-slate-11 transition-colors duration-200 -ml-3"
@click="isExpanded = !isExpanded"
>
<Icon
:icon="isExpanded ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
class="w-4 h-4 transition-transform duration-200 group-hover:scale-110"
/>
<span class="flex items-center gap-2">
{{ t('CAPTAIN.COPILOT.SHOW_STEPS') }}
<span
class="inline-flex items-center justify-center h-4 min-w-4 px-1 text-xs font-medium rounded-full bg-n-solid-3 text-n-slate-11"
>
{{ thinkingCount }}
</span>
</span>
</button>
<div
v-show="isExpanded"
class="space-y-3 transition-all duration-200"
:class="{
'opacity-100': isExpanded,
'opacity-0 max-h-0 overflow-hidden': !isExpanded,
}"
>
<CopilotThinkingBlock
v-for="message in messages"
:key="message.id"
:content="message.content"
:reasoning="message.reasoning"
/>
</div>
</div>
</template>
@@ -2,9 +2,9 @@
import { ref, computed } from 'vue';
import { OnClickOutside } from '@vueuse/components';
import { useI18n } from 'vue-i18n';
import { useMapGetter } from 'dashboard/composables/store.js';
import Button from 'dashboard/components-next/button/Button.vue';
import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
const props = defineProps({
type: {
@@ -59,8 +59,6 @@ const emit = defineEmits(['confirm', 'close']);
const { t } = useI18n();
const isRTL = useMapGetter('accounts/isRTL');
const dialogRef = ref(null);
const dialogContentRef = ref(null);
@@ -94,7 +92,7 @@ defineExpose({ open, close });
</script>
<template>
<Teleport to="body">
<TeleportWithDirection to="body">
<dialog
ref="dialogRef"
class="w-full transition-all duration-300 ease-in-out shadow-xl rounded-xl"
@@ -102,7 +100,6 @@ defineExpose({ open, close });
maxWidthClass,
overflowYAuto ? 'overflow-y-auto' : 'overflow-visible',
]"
:dir="isRTL ? 'rtl' : 'ltr'"
@close="close"
>
<OnClickOutside @trigger="close">
@@ -152,7 +149,7 @@ defineExpose({ open, close });
</form>
</OnClickOutside>
</dialog>
</Teleport>
</TeleportWithDirection>
</template>
<style scoped>
@@ -81,6 +81,7 @@ onMounted(() => {
<button
v-for="(item, index) in filteredMenuItems"
:key="index"
type="button"
class="inline-flex items-center justify-start w-full h-8 min-w-0 gap-2 px-2 py-1.5 transition-all duration-200 ease-in-out border-0 rounded-lg z-60 hover:bg-n-alpha-1 dark:hover:bg-n-alpha-2 disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50"
:class="{
'bg-n-alpha-1 dark:bg-n-solid-active': item.isSelected,
@@ -1,5 +1,6 @@
<script setup>
import { computed } from 'vue';
import { computed, ref } from 'vue';
import { useElementBounding, useWindowSize } from '@vueuse/core';
import DropdownContainer from 'next/dropdown-menu/base/DropdownContainer.vue';
import DropdownSection from 'next/dropdown-menu/base/DropdownSection.vue';
import DropdownBody from 'next/dropdown-menu/base/DropdownBody.vue';
@@ -25,6 +26,10 @@ const props = defineProps({
type: String,
default: 'faded',
},
label: {
type: String,
default: null,
},
});
const selected = defineModel({
@@ -32,6 +37,13 @@ const selected = defineModel({
required: true,
});
const triggerRef = ref(null);
const dropdownRef = ref(null);
const { top } = useElementBounding(triggerRef);
const { height } = useWindowSize();
const { height: dropdownHeight } = useElementBounding(dropdownRef);
const selectedOption = computed(() => {
return props.options.find(o => o.value === selected.value) || {};
});
@@ -41,6 +53,16 @@ const iconToRender = computed(() => {
return selectedOption.value.icon || 'i-lucide-chevron-down';
});
const dropdownPosition = computed(() => {
const DROPDOWN_MAX_HEIGHT = 340;
// Get actual height if available or use default
const menuHeight = dropdownHeight.value
? dropdownHeight.value + 20
: DROPDOWN_MAX_HEIGHT;
const spaceBelow = height.value - top.value;
return spaceBelow < menuHeight ? 'bottom-0' : 'top-0';
});
const updateSelected = newValue => {
selected.value = newValue;
};
@@ -51,17 +73,23 @@ const updateSelected = newValue => {
<template #trigger="{ toggle }">
<slot name="trigger" :toggle="toggle">
<Button
ref="triggerRef"
sm
slate
:variant
:icon="iconToRender"
:trailing-icon="selectedOption.icon ? false : true"
:label="hideLabel ? null : selectedOption.label"
:label="label || (hideLabel ? null : selectedOption.label)"
@click="toggle"
/>
</slot>
</template>
<DropdownBody class="top-0 min-w-48 z-50" strong>
<DropdownBody
ref="dropdownRef"
class="min-w-48 z-50"
:class="dropdownPosition"
strong
>
<DropdownSection class="max-h-80 overflow-scroll">
<DropdownItem
v-for="option in options"
@@ -0,0 +1,66 @@
<script setup>
import { computed, ref } from 'vue';
import Input from './Input.vue';
import { useI18n } from 'vue-i18n';
const props = defineProps({
min: { type: Number, default: 0 },
max: { type: Number, default: Infinity },
disabled: { type: Boolean, default: false },
});
const { t } = useI18n();
const duration = defineModel('modelValue', { type: Number, default: null });
const UNIT_TYPES = {
MINUTES: 'minutes',
HOURS: 'hours',
DAYS: 'days',
};
const unit = ref(UNIT_TYPES.MINUTES);
const transformedValue = computed({
get() {
if (unit.value === UNIT_TYPES.MINUTES) return duration.value;
if (unit.value === UNIT_TYPES.HOURS) return Math.floor(duration.value / 60);
if (unit.value === UNIT_TYPES.DAYS)
return Math.floor(duration.value / 24 / 60);
return 0;
},
set(newValue) {
let minuteValue;
if (unit.value === UNIT_TYPES.MINUTES) {
minuteValue = Math.floor(newValue);
} else if (unit.value === UNIT_TYPES.HOURS) {
minuteValue = Math.floor(newValue * 60);
} else if (unit.value === UNIT_TYPES.DAYS) {
minuteValue = Math.floor(newValue * 24 * 60);
}
duration.value = Math.min(Math.max(minuteValue, props.min), props.max);
},
});
</script>
<template>
<Input
v-model="transformedValue"
type="number"
autocomplete="off"
:disabled="disabled"
:placeholder="t('DURATION_INPUT.PLACEHOLDER')"
class="flex-grow w-full disabled:"
/>
<select
v-model="unit"
:disabled="disabled"
class="mb-0 text-sm disabled:outline-n-weak disabled:opacity-40"
>
<option :value="UNIT_TYPES.MINUTES">
{{ t('DURATION_INPUT.MINUTES') }}
</option>
<option :value="UNIT_TYPES.HOURS">{{ t('DURATION_INPUT.HOURS') }}</option>
<option :value="UNIT_TYPES.DAYS">{{ t('DURATION_INPUT.DAYS') }}</option>
</select>
</template>
@@ -117,7 +117,7 @@ const props = defineProps({
},
conversationId: { type: Number, required: true },
createdAt: { type: Number, required: true }, // eslint-disable-line vue/no-unused-properties
currentUserId: { type: Number, required: true },
currentUserId: { type: Number, required: true }, // eslint-disable-line vue/no-unused-properties
groupWithNext: { type: Boolean, default: false },
inboxId: { type: Number, default: null }, // eslint-disable-line vue/no-unused-properties
inboxSupportsReplyTo: { type: Object, default: () => ({}) },
@@ -173,7 +173,10 @@ const variant = computed(() => {
return variants[props.messageType] || MESSAGE_VARIANTS.USER;
});
const isMyMessage = computed(() => {
const isBotOrAgentMessage = computed(() => {
if (props.messageType === MESSAGE_TYPES.ACTIVITY) {
return false;
}
// if an outgoing message is still processing, then it's definitely a
// message sent by the current user
if (
@@ -183,16 +186,21 @@ const isMyMessage = computed(() => {
return true;
}
const senderId = props.senderId ?? props.sender?.id;
const senderType = props.senderType ?? props.sender?.type;
const senderType = props.sender?.type ?? props.senderType;
if (!senderType || !senderId) {
return false;
return true;
}
return (
senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase() &&
props.currentUserId === senderId
);
if (
[SENDER_TYPES.AGENT_BOT, SENDER_TYPES.CAPTAIN_ASSISTANT].includes(
senderType
)
) {
return true;
}
return senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase();
});
/**
@@ -200,7 +208,7 @@ const isMyMessage = computed(() => {
* @returns {import('vue').ComputedRef<'left'|'right'|'center'>} The computed orientation
*/
const orientation = computed(() => {
if (isMyMessage.value) {
if (isBotOrAgentMessage.value) {
return ORIENTATION.RIGHT;
}
@@ -221,8 +229,8 @@ const flexOrientationClass = computed(() => {
const gridClass = computed(() => {
const map = {
[ORIENTATION.LEFT]: 'grid grid-cols-[24px_1fr]',
[ORIENTATION.RIGHT]: 'grid grid-cols-1fr',
[ORIENTATION.LEFT]: 'grid grid-cols-1fr',
[ORIENTATION.RIGHT]: 'grid grid-cols-[1fr_24px]',
};
return map[orientation.value];
@@ -231,13 +239,13 @@ const gridClass = computed(() => {
const gridTemplate = computed(() => {
const map = {
[ORIENTATION.LEFT]: `
"avatar bubble"
"spacer meta"
`,
[ORIENTATION.RIGHT]: `
"bubble"
"meta"
`,
[ORIENTATION.RIGHT]: `
"bubble avatar"
"meta spacer"
`,
};
return map[orientation.value];
@@ -251,7 +259,7 @@ const shouldGroupWithNext = computed(() => {
const shouldShowAvatar = computed(() => {
if (props.messageType === MESSAGE_TYPES.ACTIVITY) return false;
if (orientation.value === ORIENTATION.RIGHT) return false;
if (orientation.value === ORIENTATION.LEFT) return false;
return true;
});
@@ -394,23 +402,29 @@ function handleReplyTo() {
}
const avatarInfo = computed(() => {
if (!props.sender || props.sender.type === SENDER_TYPES.AGENT_BOT) {
// If no sender, return bot info
if (!props.sender) {
return {
name: t('CONVERSATION.BOT'),
src: '',
};
}
if (props.sender) {
const { sender } = props;
const { name, type, avatarUrl, thumbnail } = sender || {};
// If sender type is agent bot, use avatarUrl
if ([SENDER_TYPES.AGENT_BOT, SENDER_TYPES.CAPTAIN_ASSISTANT].includes(type)) {
return {
name: props.sender.name,
src: props.sender?.thumbnail,
name: name ?? '',
src: avatarUrl ?? '',
};
}
// For all other senders, use thumbnail
return {
name: '',
src: '',
name: name ?? '',
src: thumbnail ?? '',
};
});
@@ -438,7 +452,7 @@ provideMessageContext({
isPrivate: computed(() => props.private),
variant,
orientation,
isMyMessage,
isBotOrAgentMessage,
shouldGroupWithNext,
});
</script>
@@ -470,14 +484,14 @@ provideMessageContext({
'w-full': variant === MESSAGE_VARIANTS.EMAIL,
},
]"
class="gap-x-3"
class="gap-x-2"
:style="{
gridTemplateAreas: gridTemplate,
}"
>
<div
v-if="!shouldGroupWithNext && shouldShowAvatar"
v-tooltip.right-end="avatarTooltip"
v-tooltip.left-end="avatarTooltip"
class="[grid-area:avatar] flex items-end"
>
<Avatar v-bind="avatarInfo" :size="24" />
@@ -485,7 +499,8 @@ provideMessageContext({
<div
class="[grid-area:bubble] flex"
:class="{
'ltr:pl-9 rtl:pl-0 justify-end': orientation === ORIENTATION.RIGHT,
'ltr:pl-8 rtl:pr-8 justify-end': orientation === ORIENTATION.RIGHT,
'ltr:pr-8 rtl:pl-8': orientation === ORIENTATION.LEFT,
'min-w-0': variant === MESSAGE_VARIANTS.EMAIL,
}"
@contextmenu="openContextMenu($event)"
@@ -14,7 +14,7 @@ const readableTime = computed(() =>
<template>
<BaseBubble
v-tooltip.top="readableTime"
class="px-2 py-0.5 !rounded-full flex min-w-0 items-center gap-2"
class="px-3 py-1 !rounded-xl flex min-w-0 items-center gap-2"
data-bubble-name="activity"
>
<span v-dompurify-html="content" :title="content" />
@@ -2,10 +2,10 @@
import { computed } from 'vue';
import BaseBubble from './Base.vue';
import { useI18n } from 'vue-i18n';
import { CSAT_RATINGS } from 'shared/constants/messages';
import { CSAT_RATINGS, CSAT_DISPLAY_TYPES } from 'shared/constants/messages';
import { useMessageContext } from '../provider.js';
const { contentAttributes } = useMessageContext();
const { contentAttributes, content } = useMessageContext();
const { t } = useI18n();
const response = computed(() => {
@@ -16,6 +16,14 @@ const isRatingSubmitted = computed(() => {
return !!response.value.rating;
});
const displayType = computed(() => {
return contentAttributes.value?.displayType || CSAT_DISPLAY_TYPES.EMOJI;
});
const isStarRating = computed(() => {
return displayType.value === CSAT_DISPLAY_TYPES.STAR;
});
const rating = computed(() => {
if (isRatingSubmitted.value) {
return CSAT_RATINGS.find(
@@ -25,16 +33,33 @@ const rating = computed(() => {
return null;
});
const starRatingValue = computed(() => {
return response.value.rating || 0;
});
</script>
<template>
<BaseBubble class="px-4 py-3" data-bubble-name="csat">
<h4>{{ t('CONVERSATION.CSAT_REPLY_MESSAGE') }}</h4>
<h4>{{ content || t('CONVERSATION.CSAT_REPLY_MESSAGE') }}</h4>
<dl v-if="isRatingSubmitted" class="mt-4">
<dt class="text-n-slate-11 italic">
{{ t('CONVERSATION.RATING_TITLE') }}
</dt>
<dd>{{ t(rating.translationKey) }}</dd>
<dd v-if="!isStarRating">
{{ t(rating.translationKey) }}
</dd>
<dd v-else class="flex mt-1">
<span v-for="n in 5" :key="n" class="text-2xl mr-1">
<i
:class="[
n <= starRatingValue
? 'i-ri-star-fill text-n-amber-9'
: 'i-ri-star-line text-n-slate-10',
]"
/>
</span>
</dd>
<dt v-if="response.feedbackMessage" class="text-n-slate-11 italic mt-2">
{{ t('CONVERSATION.FEEDBACK_TITLE') }}
@@ -21,6 +21,7 @@ export const SENDER_TYPES = {
CONTACT: 'Contact',
USER: 'User',
AGENT_BOT: 'agent_bot',
CAPTAIN_ASSISTANT: 'captain_assistant',
};
export const ORIENTATION = {
@@ -98,7 +98,7 @@ const MessageControl = Symbol('MessageControl');
* @property {import('vue').Ref<Sender|null>} [sender=null] - The sender information
* @property {import('vue').ComputedRef<MessageOrientation>} orientation - The visual variant of the message
* @property {import('vue').ComputedRef<MessageVariant>} variant - The visual variant of the message
* @property {import('vue').ComputedRef<boolean>} isMyMessage - Does the message belong to the current user
* @property {import('vue').ComputedRef<boolean>} isBotOrAgentMessage - Does the message belong to the current user
* @property {import('vue').ComputedRef<boolean>} isPrivate - Proxy computed value for private
* @property {import('vue').ComputedRef<boolean>} shouldGroupWithNext - Should group with the next message or not, it is differnt from groupWithNext, this has a bypass for a failed message
*/
@@ -185,6 +185,7 @@ watch(
"
trailing-icon
:disabled="disabled"
type="button"
class="!h-[1.875rem] top-1 ltr:ml-px rtl:mr-px !px-2 outline-0 !outline-none !rounded-lg border-0 ltr:!rounded-r-none rtl:!rounded-l-none"
@click="toggleCountryDropdown"
>
@@ -15,6 +15,13 @@ const props = defineProps({
type: String,
required: true,
},
subMenuPosition: {
type: String,
default: 'right',
validator: value => {
return ['right', 'left', 'bottom'].includes(value);
},
},
});
const emit = defineEmits(['update:modelValue']);
@@ -44,14 +51,21 @@ const handleSelect = value => {
trailing-icon
color="slate"
variant="faded"
class="!w-fit"
class="!w-fit max-w-40"
:class="{ 'dark:!bg-n-alpha-2 !bg-n-slate-9/20': isOpen }"
:label="labelValue"
@click="toggleMenu"
/>
<div
v-if="isOpen"
class="absolute ltr:left-full rtl:right-full select-none max-w-48 ltr:ml-1 rtl:mr-1 flex flex-col gap-1 bg-n-alpha-3 backdrop-blur-[100px] p-1 top-0 shadow-lg rounded-lg border border-n-weak"
class="absolute select-none max-w-64 flex flex-col gap-1 bg-n-alpha-3 backdrop-blur-[100px] p-1 top-0 shadow-lg z-40 rounded-lg border border-n-weak dark:border-n-strong/50"
:class="{
'ltr:left-full rtl:right-full ltr:ml-1 rtl:mr-1':
subMenuPosition === 'right',
'ltr:right-full rtl:left-full ltr:mr-1 rtl:ml-1':
subMenuPosition === 'left',
'top-full mt-1 ltr:right-0 rtl:left-0': subMenuPosition === 'bottom',
}"
>
<Button
v-for="option in options"
@@ -4,6 +4,7 @@ import { useMapGetter, useStore } from 'dashboard/composables/store';
import wootConstants from 'dashboard/constants/globals';
import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
import { useImpersonation } from 'dashboard/composables/useImpersonation';
import {
DropdownContainer,
@@ -20,6 +21,8 @@ const currentUserAvailability = useMapGetter('getCurrentUserAvailability');
const currentAccountId = useMapGetter('getCurrentAccountId');
const currentUserAutoOffline = useMapGetter('getCurrentUserAutoOffline');
const { isImpersonating } = useImpersonation();
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
const statusList = computed(() => {
return [
@@ -46,6 +49,10 @@ const activeStatus = computed(() => {
});
function changeAvailabilityStatus(availability) {
if (isImpersonating.value) {
useAlert(t('PROFILE_SETTINGS.FORM.AVAILABILITY.IMPERSONATING_ERROR'));
return;
}
try {
store.dispatch('updateAvailability', {
availability,
@@ -27,10 +27,10 @@ const updateValue = () => {
>
<span class="sr-only">{{ t('SWITCH.TOGGLE') }}</span>
<span
class="absolute top-[0.07rem] left-0.5 h-3 w-3 transform rounded-full shadow-sm transition-transform duration-200 ease-in-out"
class="absolute top-0.5 left-0.5 h-3 w-3 transform rounded-full shadow-sm transition-transform duration-200 ease-in-out"
:class="
modelValue
? 'translate-x-2.5 bg-white'
? 'translate-x-3 bg-white'
: 'translate-x-0 bg-white dark:bg-n-black'
"
/>
@@ -8,6 +8,7 @@ import {
computed,
watch,
onMounted,
onUnmounted,
defineEmits,
} from 'vue';
import { useStore } from 'vuex';
@@ -29,6 +30,7 @@ import ConversationItem from './ConversationItem.vue';
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
import IntersectionObserver from './IntersectionObserver.vue';
import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useAlert } from 'dashboard/composables';
@@ -42,7 +44,7 @@ import {
useSnakeCase,
} from 'dashboard/composables/useTransformKeys';
import { useEmitter } from 'dashboard/composables/emitter';
import { useEventListener } from '@vueuse/core';
import { useEventListener, useScrollLock } from '@vueuse/core';
import { emitter } from 'shared/helpers/mitt';
@@ -85,6 +87,12 @@ const store = useStore();
const conversationListRef = ref(null);
const conversationDynamicScroller = ref(null);
const conversationListScrollableElement = computed(
() => conversationDynamicScroller.value?.$el
);
const conversationListScrollLock = useScrollLock(
conversationListScrollableElement
);
const activeAssigneeTab = ref(wootConstants.ASSIGNEE_TYPE.ME);
const activeStatus = ref(wootConstants.STATUS_TYPE.OPEN);
@@ -738,6 +746,7 @@ function allSelectedConversationsStatus(status) {
function onContextMenuToggle(state) {
isContextMenuOpen.value = state;
conversationListScrollLock.value = state;
}
function toggleSelectAll(check) {
@@ -761,6 +770,10 @@ onMounted(() => {
}
});
onUnmounted(() => {
conversationListScrollLock.value = false;
});
provide('selectConversation', selectConversation);
provide('deSelectConversation', deSelectConversation);
provide('assignAgent', onAssignAgent);
@@ -828,14 +841,17 @@ watch(conversationFilters, (newVal, oldVal) => {
@basic-filter-change="onBasicFilterChange"
/>
<Teleport v-if="showAddFoldersModal" to="#saveFilterTeleportTarget">
<TeleportWithDirection
v-if="showAddFoldersModal"
to="#saveFilterTeleportTarget"
>
<SaveCustomView
v-model="appliedFilter"
:custom-views-query="foldersQuery"
:open-last-saved-item="openLastSavedItemInFolder"
@close="onCloseAddFoldersModal"
/>
</Teleport>
</TeleportWithDirection>
<DeleteCustomViews
v-if="showDeleteFoldersModal"
@@ -932,7 +948,10 @@ watch(conversationFilters, (newVal, oldVal) => {
</template>
</DynamicScroller>
</div>
<Teleport v-if="showAdvancedFilters" to="#conversationFilterTeleportTarget">
<TeleportWithDirection
v-if="showAdvancedFilters"
to="#conversationFilterTeleportTarget"
>
<ConversationFilter
v-model="appliedFilter"
:folder-name="activeFolderName"
@@ -941,6 +960,6 @@ watch(conversationFilters, (newVal, oldVal) => {
@update-folder="onUpdateSavedFilter"
@close="closeAdvanceFiltersModal"
/>
</Teleport>
</TeleportWithDirection>
</div>
</template>
@@ -49,12 +49,12 @@ export default {
if (this.isAttributeTypeDate) {
return this.value
? new Date(this.value || new Date()).toLocaleDateString()
: '';
: '---';
}
if (this.isAttributeTypeCheckbox) {
return this.value === 'false' ? false : this.value;
}
return this.value;
return this.hasValue ? this.value : '---';
},
formattedValue() {
return this.isAttributeTypeDate
@@ -83,6 +83,9 @@ export default {
isAttributeTypeDate() {
return this.attributeType === 'date';
},
hasValue() {
return this.value !== null && this.value !== '';
},
urlValue() {
return isValidURL(this.value) ? this.value : '---';
},
@@ -223,7 +226,7 @@ export default {
/>
</span>
<NextButton
v-if="showActions && value"
v-if="showActions && hasValue"
v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
slate
sm
@@ -281,13 +284,13 @@ export default {
v-else
class="group-hover:bg-n-slate-3 group-hover:dark:bg-n-solid-3 inline-block rounded-sm mb-0 break-all py-0.5 px-1"
>
{{ displayValue || '---' }}
{{ displayValue }}
</p>
<div
class="flex items-center max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0"
>
<NextButton
v-if="showActions && value"
v-if="showActions && hasValue"
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
xs
slate
@@ -1,6 +1,7 @@
<script>
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useImpersonation } from 'dashboard/composables/useImpersonation';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
@@ -20,6 +21,10 @@ export default {
AvailabilityStatusBadge,
NextButton,
},
setup() {
const { isImpersonating } = useImpersonation();
return { isImpersonating };
},
data() {
return {
isStatusMenuOpened: false,
@@ -73,6 +78,13 @@ export default {
});
},
changeAvailabilityStatus(availability) {
if (this.isImpersonating) {
useAlert(
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.IMPERSONATING_ERROR')
);
return;
}
if (this.isUpdating) {
return;
}
@@ -2,6 +2,8 @@
import { computed, onMounted, nextTick, useTemplateRef } from 'vue';
import { useWindowSize, useElementBounding } from '@vueuse/core';
import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
const props = defineProps({
x: { type: Number, default: 0 },
y: { type: Number, default: 0 },
@@ -57,7 +59,7 @@ onMounted(() => {
</script>
<template>
<Teleport to="body">
<TeleportWithDirection to="body">
<div
ref="menuRef"
class="fixed outline-none z-[9999] cursor-pointer"
@@ -67,5 +69,5 @@ onMounted(() => {
>
<slot />
</div>
</Teleport>
</TeleportWithDirection>
</template>
@@ -1,93 +1,132 @@
<script>
import wootConstants from 'dashboard/constants/globals';
import { mapGetters } from 'vuex';
import FilterItem from './FilterItem.vue';
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useToggle } from '@vueuse/core';
import { vOnClickOutside } from '@vueuse/components';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
import wootConstants from 'dashboard/constants/globals';
import SelectMenu from 'dashboard/components-next/selectmenu/SelectMenu.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
const CHAT_STATUS_FILTER_ITEMS = Object.freeze([
'open',
'resolved',
'pending',
'snoozed',
'all',
defineProps({
isOnExpandedLayout: {
type: Boolean,
required: true,
},
});
const emit = defineEmits(['changeFilter']);
const store = useStore();
const { t } = useI18n();
const { updateUISettings } = useUISettings();
const chatStatusFilter = useMapGetter('getChatStatusFilter');
const chatSortFilter = useMapGetter('getChatSortFilter');
const [showActionsDropdown, toggleDropdown] = useToggle();
const currentStatusFilter = computed(() => {
return chatStatusFilter.value || wootConstants.STATUS_TYPE.OPEN;
});
const currentSortBy = computed(() => {
return (
chatSortFilter.value || wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC
);
});
const chatStatusOptions = computed(() => [
{
label: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.open.TEXT'),
value: 'open',
},
{
label: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.resolved.TEXT'),
value: 'resolved',
},
{
label: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.pending.TEXT'),
value: 'pending',
},
{
label: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.snoozed.TEXT'),
value: 'snoozed',
},
{
label: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.all.TEXT'),
value: 'all',
},
]);
const SORT_ORDER_ITEMS = Object.freeze([
'last_activity_at_asc',
'last_activity_at_desc',
'created_at_desc',
'created_at_asc',
'priority_desc',
'priority_asc',
'waiting_since_asc',
'waiting_since_desc',
const chatSortOptions = computed(() => [
{
label: t('CHAT_LIST.SORT_ORDER_ITEMS.last_activity_at_asc.TEXT'),
value: 'last_activity_at_asc',
},
{
label: t('CHAT_LIST.SORT_ORDER_ITEMS.last_activity_at_desc.TEXT'),
value: 'last_activity_at_desc',
},
{
label: t('CHAT_LIST.SORT_ORDER_ITEMS.created_at_desc.TEXT'),
value: 'created_at_desc',
},
{
label: t('CHAT_LIST.SORT_ORDER_ITEMS.created_at_asc.TEXT'),
value: 'created_at_asc',
},
{
label: t('CHAT_LIST.SORT_ORDER_ITEMS.priority_desc.TEXT'),
value: 'priority_desc',
},
{
label: t('CHAT_LIST.SORT_ORDER_ITEMS.priority_asc.TEXT'),
value: 'priority_asc',
},
{
label: t('CHAT_LIST.SORT_ORDER_ITEMS.waiting_since_asc.TEXT'),
value: 'waiting_since_asc',
},
{
label: t('CHAT_LIST.SORT_ORDER_ITEMS.waiting_since_desc.TEXT'),
value: 'waiting_since_desc',
},
]);
export default {
components: {
FilterItem,
NextButton,
},
props: {
isOnExpandedLayout: {
type: Boolean,
required: true,
},
},
emits: ['changeFilter'],
setup() {
const { updateUISettings } = useUISettings();
const activeChatStatusLabel = computed(
() =>
chatStatusOptions.value.find(m => m.value === chatStatusFilter.value)
?.label || ''
);
return {
updateUISettings,
};
},
data() {
return {
showActionsDropdown: false,
chatStatusItems: CHAT_STATUS_FILTER_ITEMS,
chatSortItems: SORT_ORDER_ITEMS,
};
},
computed: {
...mapGetters({
chatStatusFilter: 'getChatStatusFilter',
chatSortFilter: 'getChatSortFilter',
}),
chatStatus() {
return this.chatStatusFilter || wootConstants.STATUS_TYPE.OPEN;
const activeChatSortLabel = computed(
() =>
chatSortOptions.value.find(m => m.value === chatSortFilter.value)?.label ||
''
);
const saveSelectedFilter = (type, value) => {
updateUISettings({
conversations_filter_by: {
status: type === 'status' ? value : currentStatusFilter.value,
order_by: type === 'sort' ? value : currentSortBy.value,
},
sortFilter() {
return (
this.chatSortFilter || wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC
);
},
},
methods: {
onTabChange(value) {
this.$emit('changeFilter', value);
this.closeDropdown();
},
toggleDropdown() {
this.showActionsDropdown = !this.showActionsDropdown;
},
closeDropdown() {
this.showActionsDropdown = false;
},
onChangeFilter(value, type) {
this.$emit('changeFilter', value, type);
this.saveSelectedFilter(type, value);
},
saveSelectedFilter(type, value) {
this.updateUISettings({
conversations_filter_by: {
status: type === 'status' ? value : this.chatStatus,
order_by: type === 'sort' ? value : this.sortFilter,
},
});
},
},
});
};
const handleStatusChange = value => {
emit('changeFilter', value, 'status');
store.dispatch('setChatStatusFilter', value);
saveSelectedFilter('status', value);
};
const handleSortChange = value => {
emit('changeFilter', value, 'sort');
store.dispatch('setChatSortFilter', value);
saveSelectedFilter('sort', value);
};
</script>
@@ -99,39 +138,39 @@ export default {
slate
faded
xs
@click="toggleDropdown"
@click="toggleDropdown()"
/>
<div
v-if="showActionsDropdown"
v-on-clickaway="closeDropdown"
class="mt-1 dropdown-pane dropdown-pane--open !w-52 !p-4 top-6 border !border-n-weak dark:!border-n-weak !bg-n-alpha-3 dark:!bg-n-alpha-3 backdrop-blur-[100px]"
v-on-click-outside="() => toggleDropdown()"
class="mt-1 bg-n-alpha-3 backdrop-blur-[100px] border border-n-weak w-72 rounded-xl p-4 absolute z-40 top-full"
:class="{
'ltr:left-0 rtl:right-0': !isOnExpandedLayout,
'ltr:right-0 rtl:left-0': isOnExpandedLayout,
}"
>
<div class="flex items-center justify-between last:mt-4">
<span class="text-xs font-medium text-n-slate-12">{{
$t('CHAT_LIST.CHAT_SORT.STATUS')
}}</span>
<FilterItem
type="status"
:selected-value="chatStatus"
:items="chatStatusItems"
path-prefix="CHAT_LIST.CHAT_STATUS_FILTER_ITEMS"
@on-change-filter="onChangeFilter"
<div class="flex items-center justify-between last:mt-4 gap-2">
<span class="text-sm truncate text-n-slate-12">
{{ $t('CHAT_LIST.CHAT_SORT.STATUS') }}
</span>
<SelectMenu
:model-value="chatStatusFilter"
:options="chatStatusOptions"
:label="activeChatStatusLabel"
:sub-menu-position="isOnExpandedLayout ? 'left' : 'right'"
@update:model-value="handleStatusChange"
/>
</div>
<div class="flex items-center justify-between last:mt-4">
<span class="text-xs font-medium text-n-slate-12">{{
$t('CHAT_LIST.CHAT_SORT.ORDER_BY')
}}</span>
<FilterItem
type="sort"
:selected-value="sortFilter"
:items="chatSortItems"
path-prefix="CHAT_LIST.SORT_ORDER_ITEMS"
@on-change-filter="onChangeFilter"
<div class="flex items-center justify-between last:mt-4 gap-2">
<span class="text-sm truncate text-n-slate-12">
{{ $t('CHAT_LIST.CHAT_SORT.ORDER_BY') }}
</span>
<SelectMenu
:model-value="chatSortFilter"
:options="chatSortOptions"
:label="activeChatSortLabel"
:sub-menu-position="isOnExpandedLayout ? 'left' : 'right'"
@update:model-value="handleSortChange"
/>
</div>
</div>
@@ -11,6 +11,7 @@ import { downloadFile } from '@chatwoot/utils';
import NextButton from 'dashboard/components-next/button/Button.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
const props = defineProps({
attachment: {
@@ -166,7 +167,7 @@ onMounted(() => {
</script>
<template>
<Teleport to="body">
<TeleportWithDirection to="body">
<woot-modal
v-model:show="show"
full-width
@@ -258,7 +259,7 @@ onMounted(() => {
<div class="flex items-center justify-center w-16 shrink-0">
<NextButton
v-if="hasMoreThanOneAttachment"
icon="i-lucide-chevron-left"
icon="ltr:i-lucide-chevron-left rtl:i-lucide-chevron-right"
class="z-10"
blue
faded
@@ -324,7 +325,7 @@ onMounted(() => {
<div class="flex items-center justify-center w-16 shrink-0">
<NextButton
v-if="hasMoreThanOneAttachment"
icon="i-lucide-chevron-right"
icon="ltr:i-lucide-chevron-right rtl:i-lucide-chevron-left"
class="z-10"
blue
faded
@@ -351,5 +352,5 @@ onMounted(() => {
</footer>
</div>
</woot-modal>
</Teleport>
</TeleportWithDirection>
</template>
@@ -56,7 +56,7 @@ const unlinkIssue = () => {
<template>
<div
class="absolute flex flex-col items-start bg-white dark:bg-slate-800 z-50 px-4 py-3 border border-solid border-ash-200 w-[384px] rounded-xl gap-4 max-h-96 overflow-auto"
class="absolute flex flex-col items-start bg-n-alpha-3 backdrop-blur-[100px] z-50 px-4 py-3 border border-solid border-n-container w-[384px] rounded-xl gap-4 max-h-96 overflow-auto"
>
<div class="flex flex-col w-full">
<IssueHeader
@@ -66,37 +66,37 @@ const unlinkIssue = () => {
@unlink-issue="unlinkIssue"
/>
<span class="mt-2 text-sm font-medium text-ash-900">
<span class="mt-2 text-sm font-medium text-n-slate-12">
{{ issue.title }}
</span>
<span
v-if="issue.description"
class="mt-1 text-sm text-ash-800 line-clamp-3"
class="mt-1 text-sm text-n-slate-11 line-clamp-3"
>
{{ issue.description }}
</span>
</div>
<div class="flex flex-row items-center h-6 gap-2">
<UserAvatarWithName v-if="assignee" :user="assignee" class="py-1" />
<div v-if="assignee" class="w-px h-3 bg-ash-200" />
<div v-if="assignee" class="w-px h-3 bg-n-slate-4" />
<div class="flex items-center gap-1 py-1">
<fluent-icon
icon="status"
size="14"
:style="{ color: issue.state.color }"
/>
<h6 class="text-xs text-ash-900">
<h6 class="text-xs text-n-slate-12">
{{ issue.state.name }}
</h6>
</div>
<div v-if="priorityLabel" class="w-px h-3 bg-ash-200" />
<div v-if="priorityLabel" class="w-px h-3 bg-n-slate-4" />
<div v-if="priorityLabel" class="flex items-center gap-1 py-1">
<fluent-icon
:icon="`priority-${priorityLabel.toLowerCase()}`"
size="14"
view-box="0 0 12 12"
/>
<h6 class="text-xs text-ash-900">{{ priorityLabel }}</h6>
<h6 class="text-xs text-n-slate-12">{{ priorityLabel }}</h6>
</div>
</div>
<div v-if="labels.length" class="flex flex-wrap items-center gap-1">
@@ -111,7 +111,7 @@ const unlinkIssue = () => {
/>
</div>
<div class="flex items-center">
<span class="text-xs text-ash-800">
<span class="text-xs text-n-slate-11">
{{
$t('INTEGRATION_SETTINGS.LINEAR.ISSUE.CREATED_AT', {
createdAt: formattedDate,
@@ -100,14 +100,17 @@ onMounted(() => {
</script>
<template>
<div class="relative" :class="{ group: linkedIssue }">
<div
class="relative after:content-[''] after:h-5 after:bg-transparent after:top-5 after:w-full after:block after:absolute after:z-0"
:class="{ group: linkedIssue }"
>
<Button
v-on-clickaway="closeIssue"
v-tooltip="tooltipText"
sm
ghost
slate
class="!gap-1"
class="!gap-1 group-hover:bg-n-alpha-2"
@click="openIssue"
>
<fluent-icon
@@ -124,7 +127,7 @@ onMounted(() => {
v-if="linkedIssue"
:issue="linkedIssue.issue"
:link-id="linkedIssue.id"
class="absolute right-0 top-[32px] invisible group-hover:visible"
class="absolute right-0 top-[36px] invisible group-hover:visible"
@unlink-issue="unlinkIssue"
/>
<woot-modal
@@ -0,0 +1,37 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { useImpersonation } from '../useImpersonation';
vi.mock('shared/helpers/sessionStorage', () => ({
__esModule: true,
default: {
get: vi.fn(),
set: vi.fn(),
},
}));
import SessionStorage from 'shared/helpers/sessionStorage';
import { SESSION_STORAGE_KEYS } from 'dashboard/constants/sessionStorage';
describe('useImpersonation', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should return true if impersonation flag is set in session storage', () => {
SessionStorage.get.mockReturnValue(true);
const { isImpersonating } = useImpersonation();
expect(isImpersonating.value).toBe(true);
expect(SessionStorage.get).toHaveBeenCalledWith(
SESSION_STORAGE_KEYS.IMPERSONATION_USER
);
});
it('should return false if impersonation flag is not set in session storage', () => {
SessionStorage.get.mockReturnValue(false);
const { isImpersonating } = useImpersonation();
expect(isImpersonating.value).toBe(false);
expect(SessionStorage.get).toHaveBeenCalledWith(
SESSION_STORAGE_KEYS.IMPERSONATION_USER
);
});
});
@@ -1,6 +1,6 @@
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useMapGetter } from './store';
import { useMapGetter, useStore } from './store';
/**
* Composable for account-related operations.
@@ -12,6 +12,7 @@ export function useAccount() {
* @type {import('vue').ComputedRef<number>}
*/
const route = useRoute();
const store = useStore();
const getAccountFn = useMapGetter('accounts/getAccount');
const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud');
const isFeatureEnabledonAccount = useMapGetter(
@@ -44,6 +45,12 @@ export function useAccount() {
};
};
const updateAccount = async data => {
await store.dispatch('accounts/update', {
...data,
});
};
return {
accountId,
route,
@@ -52,5 +59,6 @@ export function useAccount() {
accountScopedRoute,
isCloudFeatureEnabled,
isOnChatwootCloud,
updateAccount,
};
}
@@ -0,0 +1,10 @@
import { computed } from 'vue';
import SessionStorage from 'shared/helpers/sessionStorage';
import { SESSION_STORAGE_KEYS } from 'dashboard/constants/sessionStorage';
export function useImpersonation() {
const isImpersonating = computed(() => {
return SessionStorage.get(SESSION_STORAGE_KEYS.IMPERSONATION_USER);
});
return { isImpersonating };
}
@@ -6,6 +6,7 @@ export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = Object.freeze([
{ name: 'macros' },
{ name: 'conversation_info' },
{ name: 'contact_attributes' },
{ name: 'contact_notes' },
{ name: 'previous_conversation' },
{ name: 'conversation_participants' },
{ name: 'shopify_orders' },
@@ -0,0 +1,3 @@
export const SESSION_STORAGE_KEYS = {
IMPERSONATION_USER: 'impersonationUser',
};
@@ -2,23 +2,13 @@
"AGENT_BOTS": {
"HEADER": "Bots",
"LOADING_EDITOR": "Loading editor...",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.You can manage your bots from this page or create new ones using the 'Configure new bot' button.",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.",
"LEARN_MORE": "Learn about agent bots",
"CSML_BOT_EDITOR": {
"NAME": {
"LABEL": "Bot name",
"PLACEHOLDER": "Name your bot.",
"ERROR": "Bot name is required."
},
"DESCRIPTION": {
"LABEL": "Bot description",
"PLACEHOLDER": "What does this bot do?"
},
"BOT_CONFIG": {
"ERROR": "Please enter your CSML bot configuration above.",
"API_ERROR": "Your CSML configuration is invalid. Please fix it and try again."
},
"SUBMIT": "Validate and save"
"GLOBAL_BOT": "System bot",
"GLOBAL_BOT_BADGE": "System",
"AVATAR": {
"SUCCESS_DELETE": "Bot avatar deleted successfully",
"ERROR_DELETE": "Error deleting bot avatar, please try again"
},
"BOT_CONFIGURATION": {
"TITLE": "Select an agent bot",
@@ -32,7 +22,7 @@
"SELECT_PLACEHOLDER": "Select bot"
},
"ADD": {
"TITLE": "Configure new bot",
"TITLE": "Add Bot",
"CANCEL_BUTTON_TEXT": "Cancel",
"API": {
"SUCCESS_MESSAGE": "Bot added successfully.",
@@ -40,16 +30,22 @@
}
},
"LIST": {
"404": "No bots found. You can create a bot by clicking the 'Configure new bot' button",
"404": "No bots found. You can create a bot by clicking the 'Add Bot' button.",
"LOADING": "Fetching bots...",
"TYPE": "Bot type"
"TABLE_HEADER": {
"DETAILS": "Bot Details",
"URL": "Webhook URL"
}
},
"DELETE": {
"BUTTON_TEXT": "Delete",
"TITLE": "Delete bot",
"SUBMIT": "Delete",
"CANCEL_BUTTON_TEXT": "Cancel",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible.",
"CONFIRM": {
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure you want to delete {name}?",
"YES": "Yes, Delete",
"NO": "No, Keep"
},
"API": {
"SUCCESS_MESSAGE": "Bot deleted successfully.",
"ERROR_MESSAGE": "Could not delete bot. Please try again."
@@ -57,17 +53,44 @@
},
"EDIT": {
"BUTTON_TEXT": "Edit",
"LOADING": "Fetching bots...",
"TITLE": "Edit bot",
"CANCEL_BUTTON_TEXT": "Cancel",
"API": {
"SUCCESS_MESSAGE": "Bot updated successfully.",
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"FORM": {
"AVATAR": {
"LABEL": "Bot avatar"
},
"NAME": {
"LABEL": "Bot name",
"PLACEHOLDER": "Enter bot name",
"REQUIRED": "Bot name is required"
},
"DESCRIPTION": {
"LABEL": "Description",
"PLACEHOLDER": "What does this bot do?"
},
"WEBHOOK_URL": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "https://example.com/webhook",
"REQUIRED": "Webhook URL is required"
},
"ERRORS": {
"NAME": "Bot name is required",
"URL": "Webhook URL is required",
"VALID_URL": "Please enter a valid URL starting with http:// or https://"
},
"CANCEL": "Cancel",
"CREATE": "Create Bot",
"UPDATE": "Update Bot"
},
"WEBHOOK": {
"DESCRIPTION": "Configure a webhook bot to integrate with your custom services. The bot will receive and process events from conversations and can respond to them."
},
"TYPES": {
"WEBHOOK": "Webhook bot",
"CSML": "CSML bot"
"WEBHOOK": "Webhook bot"
}
}
}
@@ -126,6 +126,44 @@
"ATLEAST_ONE_CONDITION_REQUIRED": "At least one condition is required",
"ATLEAST_ONE_ACTION_REQUIRED": "At least one action is required"
},
"NONE_OPTION": "None"
"NONE_OPTION": "None",
"EVENTS": {
"CONVERSATION_CREATED": "Conversation Created",
"CONVERSATION_UPDATED": "Conversation Updated",
"MESSAGE_CREATED": "Message Created",
"CONVERSATION_OPENED": "Conversation Opened"
},
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Mute Conversation",
"SNOOZE_CONVERSATION": "Snooze Conversation",
"RESOLVE_CONVERSATION": "Resolve Conversation",
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
"SEND_ATTACHMENT": "Send Attachment",
"SEND_MESSAGE": "Send a Message",
"CHANGE_PRIORITY": "Change Priority",
"ADD_SLA": "Add SLA"
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "Email",
"INBOX": "Inbox",
"CONVERSATION_LANGUAGE": "Conversation Language",
"PHONE_NUMBER": "Phone Number",
"STATUS": "Status",
"BROWSER_LANGUAGE": "Browser Language",
"MAIL_SUBJECT": "Email Subject",
"COUNTRY_NAME": "Country",
"REFERER_LINK": "Referrer Link",
"ASSIGNEE_NAME": "Assignee",
"TEAM_NAME": "Team",
"PRIORITY": "Priority"
}
}
}
@@ -43,5 +43,11 @@
"FEATURE_SPOTLIGHT": {
"LEARN_MORE": "Learn more",
"WATCH_VIDEO": "Watch video"
},
"DURATION_INPUT": {
"MINUTES": "Minutes",
"HOURS": "Hours",
"DAYS": "Days",
"PLACEHOLDER": "Enter duration"
}
}
@@ -544,6 +544,9 @@
"WROTE": "wrote",
"YOU": "You",
"SAVE": "Save note",
"EXPAND": "Expand",
"COLLAPSE": "Collapse",
"NO_NOTES": "No notes, you can add notes from the contact details page.",
"EMPTY_STATE": "There are no notes associated to this contact. You can add a note by typing in the box above."
}
},
@@ -32,10 +32,12 @@
"LOADING_CONVERSATIONS": "Loading Conversations",
"CANNOT_REPLY": "You cannot reply due to",
"24_HOURS_WINDOW": "24 hour message window restriction",
"API_HOURS_WINDOW": "You can only reply to this conversation within {hours} hours",
"NOT_ASSIGNED_TO_YOU": "This conversation is not assigned to you. Would you like to assign this conversation to yourself?",
"ASSIGN_TO_ME": "Assign to me",
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction",
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. All new messages will show up there. You wont be able to send messages from this conversation anymore.",
"REPLYING_TO": "You are replying to:",
"REMOVE_SELECTION": "Remove Selection",
"DOWNLOAD": "Download",
@@ -293,6 +295,7 @@
"CONVERSATION_ACTIONS": "Conversation Actions",
"CONVERSATION_LABELS": "Conversation Labels",
"CONVERSATION_INFO": "Conversation Information",
"CONTACT_NOTES": "Contact Notes",
"CONTACT_ATTRIBUTES": "Contact Attributes",
"PREVIOUS_CONVERSATION": "Previous Conversations",
"MACROS": "Macros",
@@ -1,5 +1,11 @@
{
"GENERAL_SETTINGS": {
"LIMIT_MESSAGES": {
"CONVERSATION": "You have exceeded the conversation limit. Hacker plan allows only 500 conversations.",
"INBOXES": "You have exceeded the inbox limit. Hacker plan only supports website live-chat. Additional inboxes like email, WhatsApp etc. require a paid plan.",
"AGENTS": "You have exceeded the agent limit. Hacker plan allows only 2 agents.",
"NON_ADMIN": "Please contact your administrator to upgrade the plan and continue using all features."
},
"TITLE": "Account settings",
"SUBMIT": "Update settings",
"BACK": "Back",
@@ -8,6 +14,26 @@
"ERROR": "Could not update settings, try again!",
"SUCCESS": "Successfully updated account settings"
},
"ACCOUNT_DELETE_SECTION": {
"TITLE": "Delete your Account",
"NOTE": "Once you delete your account, all your data will be deleted.",
"BUTTON_TEXT": "Delete Your Account",
"CONFIRM": {
"TITLE": "Delete Account",
"MESSAGE": "Deleting your Account is irreversible. Enter your account name below to confirm you want to permanently delete it.",
"BUTTON_TEXT": "Delete",
"DISMISS": "Cancel",
"PLACE_HOLDER": "Please type {accountName} to confirm"
},
"SUCCESS": "Account marked for deletion",
"FAILURE": "Could not delete account, try again!",
"SCHEDULED_DELETION": {
"TITLE": "Account Scheduled for Deletion",
"MESSAGE_MANUAL": "This account is scheduled for deletion on {deletionDate}. This was requested by an administrator. You can cancel the deletion before this date.",
"MESSAGE_INACTIVITY": "This account is scheduled for deletion on {deletionDate} due to account inactivity. You can cancel the deletion before this date.",
"CLEAR_BUTTON": "Cancel Scheduled Deletion"
}
},
"FORM": {
"ERROR": "Please fix form errors",
"GENERAL_SECTION": {
@@ -18,6 +44,10 @@
"TITLE": "Account ID",
"NOTE": "This ID is required if you are building an API based integration"
},
"AUTO_RESOLVE": {
"TITLE": "Auto-resolve conversations",
"NOTE": "This configuration would allow you to automatically resolve the conversation after a certain period. Set the duration and customize the message to the user below."
},
"NAME": {
"LABEL": "Account name",
"PLACEHOLDER": "Your account name",
@@ -38,10 +68,23 @@
"PLACEHOLDER": "Your company's support email",
"ERROR": ""
},
"AUTO_RESOLVE_IGNORE_WAITING": {
"LABEL": "Exclude unattended conversations",
"HELP": "When enabled, the system will skip resolving conversations that are still waiting for an agents reply."
},
"AUTO_RESOLVE_DURATION": {
"LABEL": "Number of days after a ticket should auto resolve if there is no activity",
"LABEL": "Inactivity duration for resolution",
"HELP": "Duration after a conversation should auto resolve if there is no activity",
"PLACEHOLDER": "30",
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)"
"ERROR": "Auto resolve duration should be between 10 minutes and 999 days",
"API": {
"SUCCESS": "Auto resolve settings updated successfully",
"ERROR": "Failed to update auto resolve settings"
},
"UPDATE_BUTTON": "Update",
"MESSAGE_LABEL": "Custom resolution message",
"MESSAGE_PLACEHOLDER": "Conversation was marked resolved by system due to 15 days of inactivity",
"MESSAGE_HELP": "This message is sent to the customer when a conversation is automatically resolved by the system due to inactivity."
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "Conversation continuity with emails is enabled for your account.",
@@ -51,6 +94,7 @@
"UPDATE_CHATWOOT": "An update {latestChatwootVersion} for Chatwoot is available. Please update your instance.",
"LEARN_MORE": "Learn more",
"PAYMENT_PENDING": "Your payment is pending. Please update your payment information to continue using Chatwoot",
"UPGRADE": "Upgrade to continue using Chatwoot",
"LIMITS_UPGRADE": "Your account has exceeded the usage limits, please upgrade your plan to continue using Chatwoot",
"OPEN_BILLING": "Open billing"
},
@@ -696,7 +696,8 @@
"SLUG": {
"LABEL": "Slug",
"PLACEHOLDER": "user-guide",
"ERROR": "Slug is required"
"ERROR": "Slug is required",
"FORMAT_ERROR": "Please enter a valid slug, for eg: user-guide"
}
},
"PORTAL_SETTINGS": {
@@ -43,7 +43,17 @@
"INBOX_NAME": "Inbox Name",
"ADD_NAME": "Add a name for your inbox",
"PICK_NAME": "Pick a Name for your Inbox",
"PICK_A_VALUE": "Pick a value"
"PICK_A_VALUE": "Pick a value",
"CREATE_INBOX": "Create Inbox"
},
"INSTAGRAM": {
"CONTINUE_WITH_INSTAGRAM": "Continue with Instagram",
"CONNECT_YOUR_INSTAGRAM_PROFILE": "Connect your Instagram Profile",
"HELP": "To add your Instagram profile as a channel, you need to authenticate your Instagram Profile by clicking on 'Continue with Instagram' ",
"ERROR_MESSAGE": "There was an error connecting to Instagram, please try again",
"ERROR_AUTH": "There was an error connecting to Instagram, please try again",
"NEW_INBOX_SUGGESTION": "This Instagram account was previously linked to a different inbox and has now been migrated here. All new messages will appear here. The old inbox will no longer be able to send or receive messages for this account.",
"DUPLICATE_INBOX_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. You wont be able to send/receive Instagram messages from this inbox anymore."
},
"TWITTER": {
"HELP": "To add your Twitter profile as a channel, you need to authenticate your Twitter Profile by clicking on 'Sign in with Twitter' ",
@@ -471,7 +481,8 @@
"PRE_CHAT_FORM": "Pre Chat Form",
"BUSINESS_HOURS": "Business Hours",
"WIDGET_BUILDER": "Widget Builder",
"BOT_CONFIGURATION": "Bot Configuration"
"BOT_CONFIGURATION": "Bot Configuration",
"CSAT": "CSAT"
},
"SETTINGS": "Settings",
"FEATURES": {
@@ -492,9 +503,7 @@
"ENABLE_EMAIL_COLLECT_BOX": "Enable email collect box",
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "Enable or disable email collect box on new conversation",
"AUTO_ASSIGNMENT": "Enable auto assignment",
"ENABLE_CSAT": "Enable CSAT",
"SENDER_NAME_SECTION": "Enable Agent Name in Email",
"ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation",
"SENDER_NAME_SECTION_TEXT": "Enable/Disable showing Agent's name in email, if disabled it will show business name",
"ENABLE_CONTINUITY_VIA_EMAIL": "Enable conversation continuity via email",
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "Conversations will continue over email if the contact email address is available.",
@@ -568,6 +577,32 @@
"LABEL": "Visitors should provide their name and email address before starting the chat"
}
},
"CSAT": {
"TITLE": "Enable CSAT",
"SUBTITLE": "Automatically trigger CSAT surveys at the end of conversations to understand how customers feel about their support experience. Track satisfaction trends and identify areas for improvement over time.",
"DISPLAY_TYPE": {
"LABEL": "Display type"
},
"MESSAGE": {
"LABEL": "Message",
"PLACEHOLDER": "Please enter a message to show users with the form"
},
"SURVEY_RULE": {
"LABEL": "Survey rule",
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
"DESCRIPTION_SUFFIX": "any of the labels",
"OPERATOR": {
"CONTAINS": "contains",
"DOES_NOT_CONTAINS": "does not contain"
},
"SELECT_PLACEHOLDER": "select labels"
},
"NOTE": "Note: CSAT surveys are sent only once per conversation",
"API": {
"SUCCESS_MESSAGE": "CSAT settings updated successfully",
"ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
}
},
"BUSINESS_HOURS": {
"TITLE": "Set your availability",
"SUBTITLE": "Set your availability on your livechat widget",
@@ -753,7 +788,8 @@
"EMAIL": "Email",
"TELEGRAM": "Telegram",
"LINE": "Line",
"API": "API Channel"
"API": "API Channel",
"INSTAGRAM": "Instagram"
}
}
}
@@ -41,7 +41,9 @@
"MESSAGE_UPDATED": "Message updated",
"WEBWIDGET_TRIGGERED": "Live chat widget opened by the user",
"CONTACT_CREATED": "Contact created",
"CONTACT_UPDATED": "Contact updated"
"CONTACT_UPDATED": "Contact updated",
"CONVERSATION_TYPING_ON": "Conversation Typing On",
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"END_POINT": {
@@ -329,11 +331,34 @@
"HEADER_KNOW_MORE": "Know more",
"COPILOT": {
"SEND_MESSAGE": "Send message...",
"EMPTY_MESSAGE": "There was an error generating the response. Please try again.",
"LOADER": "Captain is thinking",
"YOU": "You",
"USE": "Use this",
"RESET": "Reset",
"SELECT_ASSISTANT": "Select Assistant"
"SELECT_ASSISTANT": "Select Assistant",
"PROMPTS": {
"SUMMARIZE": {
"LABEL": "Summarize this conversation",
"CONTENT": "Summarize the key points discussed between the customer and the support agent, including the customer's concerns, questions, and the solutions or responses provided by the support agent"
},
"SUGGEST": {
"LABEL": "Suggest an answer",
"CONTENT": "Analyze the customer's inquiry, and draft a response that effectively addresses their concerns or questions. Ensure the reply is clear, concise, and provides helpful information."
},
"RATE": {
"LABEL": "Rate this conversation",
"CONTENT": "Review the conversation to see how well it meets the customer's needs. Share a rating out of 5 based on tone, clarity, and effectiveness."
}
}
},
"PLAYGROUND": {
"USER": "You",
"ASSISTANT": "Assistant",
"MESSAGE_PLACEHOLDER": "Type your message...",
"HEADER": "Playground",
"DESCRIPTION": "Use this playground to send messages to your assistant and check if it responds accurately, quickly, and in the tone you expect.",
"CREDIT_NOTE": "Messages sent here will count toward your Captain credits."
},
"PAYWALL": {
"TITLE": "Upgrade to use Captain AI",
@@ -373,21 +398,45 @@
"ERROR_MESSAGE": "There was an error creating the assistant, please try again."
},
"FORM": {
"UPDATE": "Update",
"SECTIONS": {
"BASIC_INFO": "Basic Information",
"SYSTEM_MESSAGES": "System Messages",
"INSTRUCTIONS": "Instructions",
"FEATURES": "Features",
"TOOLS": "Tools "
},
"NAME": {
"LABEL": "Assistant Name",
"PLACEHOLDER": "Enter a name for the assistant",
"ERROR": "Please provide a name for the assistant"
"LABEL": "Name",
"PLACEHOLDER": "Enter assistant name",
"ERROR": "The name is required"
},
"DESCRIPTION": {
"LABEL": "Assistant Description",
"PLACEHOLDER": "Describe how and where this assistant will be used",
"ERROR": "A description is required"
"LABEL": "Description",
"PLACEHOLDER": "Enter assistant description",
"ERROR": "The description is required"
},
"PRODUCT_NAME": {
"LABEL": "Product Name",
"PLACEHOLDER": "Enter the name of the product this assistant is designed for",
"PLACEHOLDER": "Enter product name",
"ERROR": "The product name is required"
},
"WELCOME_MESSAGE": {
"LABEL": "Welcome Message",
"PLACEHOLDER": "Enter welcome message"
},
"HANDOFF_MESSAGE": {
"LABEL": "Handoff Message",
"PLACEHOLDER": "Enter handoff message"
},
"RESOLUTION_MESSAGE": {
"LABEL": "Resolution Message",
"PLACEHOLDER": "Enter resolution message"
},
"INSTRUCTIONS": {
"LABEL": "Instructions",
"PLACEHOLDER": "Enter instructions for the assistant"
},
"FEATURES": {
"TITLE": "Features",
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
@@ -397,7 +446,8 @@
"EDIT": {
"TITLE": "Update the assistant",
"SUCCESS_MESSAGE": "The assistant has been successfully updated",
"ERROR_MESSAGE": "There was an error updating the assistant, please try again."
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"OPTIONS": {
"EDIT_ASSISTANT": "Edit Assistant",
@@ -83,6 +83,22 @@
"ACTION_PARAMETERS_REQUIRED": "Action parameters are required",
"ATLEAST_ONE_CONDITION_REQUIRED": "At least one condition is required",
"ATLEAST_ONE_ACTION_REQUIRED": "At least one action is required"
},
"ACTIONS": {
"ASSIGN_TEAM": "Assign a Team",
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Mute Conversation",
"SNOOZE_CONVERSATION": "Snooze Conversation",
"RESOLVE_CONVERSATION": "Resolve Conversation",
"SEND_ATTACHMENT": "Send Attachment",
"SEND_MESSAGE": "Send a Message",
"CHANGE_PRIORITY": "Change Priority",
"ADD_PRIVATE_NOTE": "Add a Private Note",
"SEND_WEBHOOK_EVENT": "Send Webhook Event"
}
}
}
@@ -387,7 +387,8 @@
"LABEL": "Company Name",
"PLACEHOLDER": "Wayne Enterprises"
},
"SUBMIT": "Submit"
"SUBMIT": "Submit",
"CANCEL": "Cancel"
}
},
"KEYBOARD_SHORTCUTS": {
@@ -2,23 +2,13 @@
"AGENT_BOTS": {
"HEADER": "الروبوتات",
"LOADING_EDITOR": "جار جلب المحرر...",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.You can manage your bots from this page or create new ones using the 'Configure new bot' button.",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.",
"LEARN_MORE": "Learn about agent bots",
"CSML_BOT_EDITOR": {
"NAME": {
"LABEL": "اسم الروبوت",
"PLACEHOLDER": "قم بتسمية الروبوت الخاص بك.",
"ERROR": "اسم الروبوت مطلوب."
},
"DESCRIPTION": {
"LABEL": "وصف الروبوت",
"PLACEHOLDER": "ماذا يفعل هذا الروبوت؟"
},
"BOT_CONFIG": {
"ERROR": "يرجى إدخال تكوين نبوت CSML الخاص بك أعلاه.",
"API_ERROR": "تكوين CSML الخاص بك غير صالح. يرجى إصلاحه والمحاولة مرة أخرى."
},
"SUBMIT": "التحقق والحفظ"
"GLOBAL_BOT": "System bot",
"GLOBAL_BOT_BADGE": "النظام",
"AVATAR": {
"SUCCESS_DELETE": "Bot avatar deleted successfully",
"ERROR_DELETE": "Error deleting bot avatar, please try again"
},
"BOT_CONFIGURATION": {
"TITLE": "اختر الروبوت",
@@ -32,7 +22,7 @@
"SELECT_PLACEHOLDER": "اختر الروبوت"
},
"ADD": {
"TITLE": "تكوين روبوت جديد",
"TITLE": "Add Bot",
"CANCEL_BUTTON_TEXT": "إلغاء",
"API": {
"SUCCESS_MESSAGE": "تمت إضافة الروبوت بنجاح.",
@@ -40,16 +30,22 @@
}
},
"LIST": {
"404": "لم يتم العثور على أي روبوتات. يمكنك إنشاء الروبوت بالنقر على زر 'تكوين روبوت جديد' ↗",
"404": "No bots found. You can create a bot by clicking the 'Add Bot' button.",
"LOADING": "جار جلب الروبوتات...",
"TYPE": "نوع الروبوت"
"TABLE_HEADER": {
"DETAILS": "Bot Details",
"URL": "رابط Webhook"
}
},
"DELETE": {
"BUTTON_TEXT": "حذف",
"TITLE": "حذف الروبوت",
"SUBMIT": "حذف",
"CANCEL_BUTTON_TEXT": "إلغاء",
"DESCRIPTION": "هل أنت متأكد أنك تريد حذف هذا الروبوت؟ هذا الإجراء لا يمكن التراجع عنه.",
"CONFIRM": {
"TITLE": "تأكيد الحذف",
"MESSAGE": "Are you sure you want to delete {name}?",
"YES": "نعم، احذف",
"NO": "لا، احتفظ"
},
"API": {
"SUCCESS_MESSAGE": "تم حذف الروبوت بنجاح.",
"ERROR_MESSAGE": "تعذر حذف الروبوت. يرجى المحاولة مرة أخرى."
@@ -57,17 +53,44 @@
},
"EDIT": {
"BUTTON_TEXT": "تعديل",
"LOADING": "جار جلب الروبوتات...",
"TITLE": "تعديل الروبوت",
"CANCEL_BUTTON_TEXT": "إلغاء",
"API": {
"SUCCESS_MESSAGE": "تم تحديث الروبوت بنجاح.",
"ERROR_MESSAGE": "تعذر تحديث الروبوت. يرجى المحاولة مرة أخرى."
}
},
"FORM": {
"AVATAR": {
"LABEL": "Bot avatar"
},
"NAME": {
"LABEL": "اسم الروبوت",
"PLACEHOLDER": "Enter bot name",
"REQUIRED": "اسم الروبوت مطلوب"
},
"DESCRIPTION": {
"LABEL": "الوصف",
"PLACEHOLDER": "ماذا يفعل هذا الروبوت؟"
},
"WEBHOOK_URL": {
"LABEL": "رابط Webhook",
"PLACEHOLDER": "https://example.com/webhook",
"REQUIRED": "Webhook URL is required"
},
"ERRORS": {
"NAME": "اسم الروبوت مطلوب",
"URL": "Webhook URL is required",
"VALID_URL": "Please enter a valid URL starting with http:// or https://"
},
"CANCEL": "إلغاء",
"CREATE": "Create Bot",
"UPDATE": "Update Bot"
},
"WEBHOOK": {
"DESCRIPTION": "Configure a webhook bot to integrate with your custom services. The bot will receive and process events from conversations and can respond to them."
},
"TYPES": {
"WEBHOOK": "روبوت الـWebhook",
"CSML": "بوت CSML"
"WEBHOOK": "روبوت الـWebhook"
}
}
}
@@ -126,6 +126,44 @@
"ATLEAST_ONE_CONDITION_REQUIRED": "شرط واحد على الأقل مطلوب",
"ATLEAST_ONE_ACTION_REQUIRED": "إجراء واحد على الأقل مطلوب"
},
"NONE_OPTION": "لا شيء"
"NONE_OPTION": "لا شيء",
"EVENTS": {
"CONVERSATION_CREATED": "تم إنشاء المحادثة",
"CONVERSATION_UPDATED": "تم تحديث المحادثة",
"MESSAGE_CREATED": "Message Created",
"CONVERSATION_OPENED": "Conversation Opened"
},
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "كتم المحادثة",
"SNOOZE_CONVERSATION": "تأجيل المحادثة",
"RESOLVE_CONVERSATION": "إعادة فتح المحادثة",
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
"SEND_ATTACHMENT": "Send Attachment",
"SEND_MESSAGE": "Send a Message",
"CHANGE_PRIORITY": "تغيير الأولوية",
"ADD_SLA": "Add SLA"
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "البريد الإلكتروني",
"INBOX": "صندوق الوارد",
"CONVERSATION_LANGUAGE": "Conversation Language",
"PHONE_NUMBER": "رقم الهاتف",
"STATUS": "الحالة",
"BROWSER_LANGUAGE": "لغة المتصفح",
"MAIL_SUBJECT": "Email Subject",
"COUNTRY_NAME": "الدولة",
"REFERER_LINK": "Referrer Link",
"ASSIGNEE_NAME": "المكلَّف",
"TEAM_NAME": "الفريق",
"PRIORITY": "الأولوية"
}
}
}
@@ -43,5 +43,11 @@
"FEATURE_SPOTLIGHT": {
"LEARN_MORE": "اعرف المزيد",
"WATCH_VIDEO": "Watch video"
},
"DURATION_INPUT": {
"MINUTES": "Minutes",
"HOURS": "Hours",
"DAYS": "Days",
"PLACEHOLDER": "Enter duration"
}
}
@@ -544,6 +544,9 @@
"WROTE": "كتب",
"YOU": "أنت",
"SAVE": "Save note",
"EXPAND": "Expand",
"COLLAPSE": "Collapse",
"NO_NOTES": "No notes, you can add notes from the contact details page.",
"EMPTY_STATE": "There are no notes associated to this contact. You can add a note by typing in the box above."
}
},
@@ -32,10 +32,12 @@
"LOADING_CONVERSATIONS": "جاري جلب المحادثات",
"CANNOT_REPLY": "لا يمكنك الرد بسبب",
"24_HOURS_WINDOW": "قيد نافذة الـ 24 ساعة",
"API_HOURS_WINDOW": "You can only reply to this conversation within {hours} hours",
"NOT_ASSIGNED_TO_YOU": "لم يتم تعيين هذه المحادثة لك. هل ترغب في تعيين هذه المحادثة لنفسك؟",
"ASSIGN_TO_ME": "إسناد لي",
"TWILIO_WHATSAPP_CAN_REPLY": "يمكنك فقط الرد على هذه المحادثة باستخدام رسالة قالب بسبب",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "قيد نافذة الـ 24 ساعة",
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. All new messages will show up there. You wont be able to send messages from this conversation anymore.",
"REPLYING_TO": "أنت ترد على:",
"REMOVE_SELECTION": "إزالة التحديد",
"DOWNLOAD": "تحميل",
@@ -293,6 +295,7 @@
"CONVERSATION_ACTIONS": "إجراءات المحادثة",
"CONVERSATION_LABELS": "وسوم المحادثة",
"CONVERSATION_INFO": "معلومات المحادثة",
"CONTACT_NOTES": "Contact Notes",
"CONTACT_ATTRIBUTES": "سمات جهة الاتصال",
"PREVIOUS_CONVERSATION": "المحادثات السابقة",
"MACROS": "ماكروس",
@@ -1,5 +1,11 @@
{
"GENERAL_SETTINGS": {
"LIMIT_MESSAGES": {
"CONVERSATION": "You have exceeded the conversation limit. Hacker plan allows only 500 conversations.",
"INBOXES": "You have exceeded the inbox limit. Hacker plan only supports website live-chat. Additional inboxes like email, WhatsApp etc. require a paid plan.",
"AGENTS": "You have exceeded the agent limit. Hacker plan allows only 2 agents.",
"NON_ADMIN": "Please contact your administrator to upgrade the plan and continue using all features."
},
"TITLE": "إعدادات الحساب",
"SUBMIT": "تحديث الإعدادات",
"BACK": "العودة",
@@ -8,6 +14,26 @@
"ERROR": "تعذر تحديث الإعدادات، الرجاء المحاولة مرة أخرى!",
"SUCCESS": "تم تحديث إعدادات الحساب بنجاح"
},
"ACCOUNT_DELETE_SECTION": {
"TITLE": "Delete your Account",
"NOTE": "Once you delete your account, all your data will be deleted.",
"BUTTON_TEXT": "Delete Your Account",
"CONFIRM": {
"TITLE": "Delete Account",
"MESSAGE": "Deleting your Account is irreversible. Enter your account name below to confirm you want to permanently delete it.",
"BUTTON_TEXT": "حذف",
"DISMISS": "إلغاء",
"PLACE_HOLDER": "الرجاء كتابة {accountName} للتأكيد"
},
"SUCCESS": "Account marked for deletion",
"FAILURE": "Could not delete account, try again!",
"SCHEDULED_DELETION": {
"TITLE": "Account Scheduled for Deletion",
"MESSAGE_MANUAL": "This account is scheduled for deletion on {deletionDate}. This was requested by an administrator. You can cancel the deletion before this date.",
"MESSAGE_INACTIVITY": "This account is scheduled for deletion on {deletionDate} due to account inactivity. You can cancel the deletion before this date.",
"CLEAR_BUTTON": "Cancel Scheduled Deletion"
}
},
"FORM": {
"ERROR": "الرجاء إصلاح الأخطاء في النموذج",
"GENERAL_SECTION": {
@@ -18,6 +44,10 @@
"TITLE": "معرف الحساب",
"NOTE": "هذا المعرف مطلوب إذا كنت بصدد بناء تكامل على API"
},
"AUTO_RESOLVE": {
"TITLE": "Auto-resolve conversations",
"NOTE": "This configuration would allow you to automatically resolve the conversation after a certain period. Set the duration and customize the message to the user below."
},
"NAME": {
"LABEL": "اسم الحساب",
"PLACEHOLDER": "اسم الحساب الخاص بك",
@@ -38,10 +68,23 @@
"PLACEHOLDER": "عنوان البريد الإلكتروني الخاص باستقبال رسائل الدعم الفني",
"ERROR": ""
},
"AUTO_RESOLVE_IGNORE_WAITING": {
"LABEL": "Exclude unattended conversations",
"HELP": "When enabled, the system will skip resolving conversations that are still waiting for an agents reply."
},
"AUTO_RESOLVE_DURATION": {
"LABEL": "عدد الأيام للتذكرة التي يجب أن يتم حلها تلقائياً إذا لم يكن هناك أي نشاط",
"LABEL": "Inactivity duration for resolution",
"HELP": "Duration after a conversation should auto resolve if there is no activity",
"PLACEHOLDER": "30",
"ERROR": "الرجاء إدخال مدة صالحة للحل تلقائي (حد أدنى 1 يوم والحد الأقصى 999 يوما)"
"ERROR": "Auto resolve duration should be between 10 minutes and 999 days",
"API": {
"SUCCESS": "Auto resolve settings updated successfully",
"ERROR": "Failed to update auto resolve settings"
},
"UPDATE_BUTTON": "تحديث",
"MESSAGE_LABEL": "Custom resolution message",
"MESSAGE_PLACEHOLDER": "Conversation was marked resolved by system due to 15 days of inactivity",
"MESSAGE_HELP": "This message is sent to the customer when a conversation is automatically resolved by the system due to inactivity."
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "الاستمرار في المحادثة عبر رسائل البريد الإلكتروني مفعّل لحسابك.",
@@ -51,6 +94,7 @@
"UPDATE_CHATWOOT": "يتوفر تحديث {latestChatwootVersion} لـ Chatwoot. الرجاء التحديث.",
"LEARN_MORE": "اعرف المزيد",
"PAYMENT_PENDING": "الدفعة الخاصة بك معلقة. الرجاء تحديث معلومات الدفع الخاصة بك للاستمرار في استخدام Chatwoot",
"UPGRADE": "Upgrade to continue using Chatwoot",
"LIMITS_UPGRADE": "لقد تجاوز حسابك حدود الاستخدام، يرجى ترقية خطتك للاستمرار في استخدام Chatwoot",
"OPEN_BILLING": "فتح الفواتير"
},
@@ -696,7 +696,8 @@
"SLUG": {
"LABEL": "Slug",
"PLACEHOLDER": "user-guide",
"ERROR": "Slug is required"
"ERROR": "Slug is required",
"FORMAT_ERROR": "Please enter a valid slug, for eg: user-guide"
}
},
"PORTAL_SETTINGS": {
@@ -43,7 +43,17 @@
"INBOX_NAME": "اسم صندوق الوارد لقناة التواصل",
"ADD_NAME": "قم بتعيين اسم لصندوق الوارد الخاص بقناتك الجديدة",
"PICK_NAME": "Pick a Name for your Inbox",
"PICK_A_VALUE": "اختر قيمة"
"PICK_A_VALUE": "اختر قيمة",
"CREATE_INBOX": "إنشاء قناة تواصل"
},
"INSTAGRAM": {
"CONTINUE_WITH_INSTAGRAM": "Continue with Instagram",
"CONNECT_YOUR_INSTAGRAM_PROFILE": "Connect your Instagram Profile",
"HELP": "To add your Instagram profile as a channel, you need to authenticate your Instagram Profile by clicking on 'Continue with Instagram' ",
"ERROR_MESSAGE": "There was an error connecting to Instagram, please try again",
"ERROR_AUTH": "There was an error connecting to Instagram, please try again",
"NEW_INBOX_SUGGESTION": "This Instagram account was previously linked to a different inbox and has now been migrated here. All new messages will appear here. The old inbox will no longer be able to send or receive messages for this account.",
"DUPLICATE_INBOX_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. You wont be able to send/receive Instagram messages from this inbox anymore."
},
"TWITTER": {
"HELP": "لإضافة حساب تويتر الخاص بك كقناة تواصل، تحتاج إلى مصادقة حسابك على تويتر بك بالنقر على زر \"تسجيل الدخول باستخدام تويتر\" ",
@@ -471,7 +481,8 @@
"PRE_CHAT_FORM": "نموذج ما قبل الدردشة",
"BUSINESS_HOURS": "ساعات العمل",
"WIDGET_BUILDER": "منشئ اللايف شات",
"BOT_CONFIGURATION": "اعدادات البوت"
"BOT_CONFIGURATION": "اعدادات البوت",
"CSAT": "تقييم رضاء العملاء"
},
"SETTINGS": "الإعدادات",
"FEATURES": {
@@ -492,9 +503,7 @@
"ENABLE_EMAIL_COLLECT_BOX": "تفعيل صندوق جمع البريد الإلكتروني",
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "تمكين أو تعطيل مربع جمع البريد الإلكتروني في محادثة جديدة",
"AUTO_ASSIGNMENT": "تفعيل الإسناد التلقائي",
"ENABLE_CSAT": "تمكين تقييم خدمة العملاء",
"SENDER_NAME_SECTION": "تمكين اسم الوكيل في البريد الإلكتروني",
"ENABLE_CSAT_SUB_TEXT": "تمكين/تعطيل تقييم خدمة العملاء بعد إنتهاء المحادثة",
"SENDER_NAME_SECTION_TEXT": "تمكين/تعطيل إظهار اسم الوكيل في البريد الإلكتروني، إذا تم تعطيله فسيظهر اسم المنشأة",
"ENABLE_CONTINUITY_VIA_EMAIL": "تمكين استمرارية المحادثة عبر البريد الإلكتروني",
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "المحادثات ستستمر عبر البريد الإلكتروني إذا كان عنوان البريد الإلكتروني لجهة الاتصال متاحاً.",
@@ -568,6 +577,32 @@
"LABEL": "يجب على الزوار تقديم اسمهم وعنوان بريدهم الإلكتروني قبل بدء المحادثة"
}
},
"CSAT": {
"TITLE": "تمكين تقييم خدمة العملاء",
"SUBTITLE": "Automatically trigger CSAT surveys at the end of conversations to understand how customers feel about their support experience. Track satisfaction trends and identify areas for improvement over time.",
"DISPLAY_TYPE": {
"LABEL": "Display type"
},
"MESSAGE": {
"LABEL": "رسالة",
"PLACEHOLDER": "Please enter a message to show users with the form"
},
"SURVEY_RULE": {
"LABEL": "Survey rule",
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
"DESCRIPTION_SUFFIX": "any of the labels",
"OPERATOR": {
"CONTAINS": "يحتوي",
"DOES_NOT_CONTAINS": "لا يحتوي"
},
"SELECT_PLACEHOLDER": "select labels"
},
"NOTE": "Note: CSAT surveys are sent only once per conversation",
"API": {
"SUCCESS_MESSAGE": "CSAT settings updated successfully",
"ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
}
},
"BUSINESS_HOURS": {
"TITLE": "قم بتعيين توافرك",
"SUBTITLE": "تعيين توافرك على أداة الدردشة المباشرة الخاصة بك",
@@ -753,7 +788,8 @@
"EMAIL": "البريد الإلكتروني",
"TELEGRAM": "تيليجرام",
"LINE": "Line",
"API": "قناة API"
"API": "قناة API",
"INSTAGRAM": "Instagram"
}
}
}
@@ -41,7 +41,9 @@
"MESSAGE_UPDATED": "تم تحديث الرسالة",
"WEBWIDGET_TRIGGERED": "أداة الدردشة المباشرة مفتوحة من قبل المستخدم",
"CONTACT_CREATED": "Contact created",
"CONTACT_UPDATED": "Contact updated"
"CONTACT_UPDATED": "Contact updated",
"CONVERSATION_TYPING_ON": "Conversation Typing On",
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"END_POINT": {
@@ -329,11 +331,34 @@
"HEADER_KNOW_MORE": "Know more",
"COPILOT": {
"SEND_MESSAGE": "إرسال الرسالة...",
"EMPTY_MESSAGE": "There was an error generating the response. Please try again.",
"LOADER": "Captain is thinking",
"YOU": "أنت",
"USE": "Use this",
"RESET": "Reset",
"SELECT_ASSISTANT": "Select Assistant"
"SELECT_ASSISTANT": "Select Assistant",
"PROMPTS": {
"SUMMARIZE": {
"LABEL": "Summarize this conversation",
"CONTENT": "Summarize the key points discussed between the customer and the support agent, including the customer's concerns, questions, and the solutions or responses provided by the support agent"
},
"SUGGEST": {
"LABEL": "Suggest an answer",
"CONTENT": "Analyze the customer's inquiry, and draft a response that effectively addresses their concerns or questions. Ensure the reply is clear, concise, and provides helpful information."
},
"RATE": {
"LABEL": "Rate this conversation",
"CONTENT": "Review the conversation to see how well it meets the customer's needs. Share a rating out of 5 based on tone, clarity, and effectiveness."
}
}
},
"PLAYGROUND": {
"USER": "أنت",
"ASSISTANT": "Assistant",
"MESSAGE_PLACEHOLDER": "أكتب رسالتك...",
"HEADER": "Playground",
"DESCRIPTION": "Use this playground to send messages to your assistant and check if it responds accurately, quickly, and in the tone you expect.",
"CREDIT_NOTE": "Messages sent here will count toward your Captain credits."
},
"PAYWALL": {
"TITLE": "Upgrade to use Captain AI",
@@ -373,21 +398,45 @@
"ERROR_MESSAGE": "There was an error creating the assistant, please try again."
},
"FORM": {
"UPDATE": "تحديث",
"SECTIONS": {
"BASIC_INFO": "Basic Information",
"SYSTEM_MESSAGES": "System Messages",
"INSTRUCTIONS": "Instructions",
"FEATURES": "الخصائص",
"TOOLS": "Tools "
},
"NAME": {
"LABEL": "Assistant Name",
"PLACEHOLDER": "Enter a name for the assistant",
"ERROR": "Please provide a name for the assistant"
"LABEL": "الاسم",
"PLACEHOLDER": "Enter assistant name",
"ERROR": "The name is required"
},
"DESCRIPTION": {
"LABEL": "Assistant Description",
"PLACEHOLDER": "Describe how and where this assistant will be used",
"ERROR": "A description is required"
"LABEL": "الوصف",
"PLACEHOLDER": "Enter assistant description",
"ERROR": "The description is required"
},
"PRODUCT_NAME": {
"LABEL": "Product Name",
"PLACEHOLDER": "Enter the name of the product this assistant is designed for",
"PLACEHOLDER": "Enter product name",
"ERROR": "The product name is required"
},
"WELCOME_MESSAGE": {
"LABEL": "Welcome Message",
"PLACEHOLDER": "Enter welcome message"
},
"HANDOFF_MESSAGE": {
"LABEL": "Handoff Message",
"PLACEHOLDER": "Enter handoff message"
},
"RESOLUTION_MESSAGE": {
"LABEL": "Resolution Message",
"PLACEHOLDER": "Enter resolution message"
},
"INSTRUCTIONS": {
"LABEL": "Instructions",
"PLACEHOLDER": "Enter instructions for the assistant"
},
"FEATURES": {
"TITLE": "الخصائص",
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
@@ -397,7 +446,8 @@
"EDIT": {
"TITLE": "Update the assistant",
"SUCCESS_MESSAGE": "The assistant has been successfully updated",
"ERROR_MESSAGE": "There was an error updating the assistant, please try again."
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"OPTIONS": {
"EDIT_ASSISTANT": "Edit Assistant",
@@ -83,6 +83,22 @@
"ACTION_PARAMETERS_REQUIRED": "معلمات الإجراء مطلوبة",
"ATLEAST_ONE_CONDITION_REQUIRED": "شرط واحد على الأقل مطلوب",
"ATLEAST_ONE_ACTION_REQUIRED": "إجراء واحد على الأقل مطلوب"
},
"ACTIONS": {
"ASSIGN_TEAM": "Assign a Team",
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "كتم المحادثة",
"SNOOZE_CONVERSATION": "تأجيل المحادثة",
"RESOLVE_CONVERSATION": "إعادة فتح المحادثة",
"SEND_ATTACHMENT": "Send Attachment",
"SEND_MESSAGE": "Send a Message",
"CHANGE_PRIORITY": "تغيير الأولوية",
"ADD_PRIVATE_NOTE": "Add a Private Note",
"SEND_WEBHOOK_EVENT": "Send Webhook Event"
}
}
}
@@ -387,7 +387,8 @@
"LABEL": "اسم المنشأة",
"PLACEHOLDER": "مؤسسة Wayne"
},
"SUBMIT": "إرسال"
"SUBMIT": "إرسال",
"CANCEL": "إلغاء"
}
},
"KEYBOARD_SHORTCUTS": {
@@ -2,23 +2,13 @@
"AGENT_BOTS": {
"HEADER": "Bots",
"LOADING_EDITOR": "Loading editor...",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.You can manage your bots from this page or create new ones using the 'Configure new bot' button.",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.",
"LEARN_MORE": "Learn about agent bots",
"CSML_BOT_EDITOR": {
"NAME": {
"LABEL": "Bot name",
"PLACEHOLDER": "Name your bot.",
"ERROR": "Bot name is required."
},
"DESCRIPTION": {
"LABEL": "Bot description",
"PLACEHOLDER": "What does this bot do?"
},
"BOT_CONFIG": {
"ERROR": "Please enter your CSML bot configuration above.",
"API_ERROR": "Your CSML configuration is invalid. Please fix it and try again."
},
"SUBMIT": "Validate and save"
"GLOBAL_BOT": "System bot",
"GLOBAL_BOT_BADGE": "System",
"AVATAR": {
"SUCCESS_DELETE": "Bot avatar deleted successfully",
"ERROR_DELETE": "Error deleting bot avatar, please try again"
},
"BOT_CONFIGURATION": {
"TITLE": "Select an agent bot",
@@ -32,7 +22,7 @@
"SELECT_PLACEHOLDER": "Select bot"
},
"ADD": {
"TITLE": "Configure new bot",
"TITLE": "Add Bot",
"CANCEL_BUTTON_TEXT": "Cancel",
"API": {
"SUCCESS_MESSAGE": "Bot added successfully.",
@@ -40,16 +30,22 @@
}
},
"LIST": {
"404": "No bots found. You can create a bot by clicking the 'Configure new bot' button",
"404": "No bots found. You can create a bot by clicking the 'Add Bot' button.",
"LOADING": "Fetching bots...",
"TYPE": "Bot type"
"TABLE_HEADER": {
"DETAILS": "Bot Details",
"URL": "Webhook URL"
}
},
"DELETE": {
"BUTTON_TEXT": "Delete",
"TITLE": "Delete bot",
"SUBMIT": "Delete",
"CANCEL_BUTTON_TEXT": "Cancel",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible.",
"CONFIRM": {
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure you want to delete {name}?",
"YES": "Yes, Delete",
"NO": "No, Keep"
},
"API": {
"SUCCESS_MESSAGE": "Bot deleted successfully.",
"ERROR_MESSAGE": "Could not delete bot. Please try again."
@@ -57,17 +53,44 @@
},
"EDIT": {
"BUTTON_TEXT": "Edit",
"LOADING": "Fetching bots...",
"TITLE": "Edit bot",
"CANCEL_BUTTON_TEXT": "Cancel",
"API": {
"SUCCESS_MESSAGE": "Bot updated successfully.",
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"FORM": {
"AVATAR": {
"LABEL": "Bot avatar"
},
"NAME": {
"LABEL": "Bot name",
"PLACEHOLDER": "Enter bot name",
"REQUIRED": "Bot name is required"
},
"DESCRIPTION": {
"LABEL": "Description",
"PLACEHOLDER": "What does this bot do?"
},
"WEBHOOK_URL": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "https://example.com/webhook",
"REQUIRED": "Webhook URL is required"
},
"ERRORS": {
"NAME": "Bot name is required",
"URL": "Webhook URL is required",
"VALID_URL": "Please enter a valid URL starting with http:// or https://"
},
"CANCEL": "Cancel",
"CREATE": "Create Bot",
"UPDATE": "Update Bot"
},
"WEBHOOK": {
"DESCRIPTION": "Configure a webhook bot to integrate with your custom services. The bot will receive and process events from conversations and can respond to them."
},
"TYPES": {
"WEBHOOK": "Webhook bot",
"CSML": "CSML bot"
"WEBHOOK": "Webhook bot"
}
}
}
@@ -126,6 +126,44 @@
"ATLEAST_ONE_CONDITION_REQUIRED": "At least one condition is required",
"ATLEAST_ONE_ACTION_REQUIRED": "At least one action is required"
},
"NONE_OPTION": "None"
"NONE_OPTION": "None",
"EVENTS": {
"CONVERSATION_CREATED": "Conversation Created",
"CONVERSATION_UPDATED": "Conversation Updated",
"MESSAGE_CREATED": "Message Created",
"CONVERSATION_OPENED": "Conversation Opened"
},
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Mute Conversation",
"SNOOZE_CONVERSATION": "Snooze Conversation",
"RESOLVE_CONVERSATION": "Resolve Conversation",
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
"SEND_ATTACHMENT": "Send Attachment",
"SEND_MESSAGE": "Send a Message",
"CHANGE_PRIORITY": "Change Priority",
"ADD_SLA": "Add SLA"
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "Email",
"INBOX": "Inbox",
"CONVERSATION_LANGUAGE": "Conversation Language",
"PHONE_NUMBER": "Phone Number",
"STATUS": "Status",
"BROWSER_LANGUAGE": "Browser Language",
"MAIL_SUBJECT": "Email Subject",
"COUNTRY_NAME": "Country",
"REFERER_LINK": "Referrer Link",
"ASSIGNEE_NAME": "Assignee",
"TEAM_NAME": "Team",
"PRIORITY": "Priority"
}
}
}

Some files were not shown because too many files have changed in this diff Show More