Files
chatwoot/app/javascript/widget/components/AgentMessageBubble.vue
Sivin Varghese 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

153 lines
4.1 KiB
Vue
Executable File

<script>
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import ChatCard from 'shared/components/ChatCard.vue';
import ChatForm from 'shared/components/ChatForm.vue';
import ChatOptions from 'shared/components/ChatOptions.vue';
import ChatArticle from './template/Article.vue';
import EmailInput from './template/EmailInput.vue';
import CustomerSatisfaction from 'shared/components/CustomerSatisfaction.vue';
import IntegrationCard from './template/IntegrationCard.vue';
export default {
name: 'AgentMessageBubble',
components: {
ChatArticle,
ChatCard,
ChatForm,
ChatOptions,
EmailInput,
CustomerSatisfaction,
IntegrationCard,
},
props: {
message: { type: String, default: null },
contentType: { type: String, default: null },
messageType: { type: Number, default: null },
messageId: { type: Number, default: null },
messageContentAttributes: {
type: Object,
default: () => {},
},
},
setup() {
const { formatMessage, getPlainText, truncateMessage, highlightContent } =
useMessageFormatter();
return {
formatMessage,
getPlainText,
truncateMessage,
highlightContent,
};
},
computed: {
isTemplate() {
return this.messageType === 3;
},
isTemplateEmail() {
return this.contentType === 'input_email';
},
isCards() {
return this.contentType === 'cards';
},
isOptions() {
return this.contentType === 'input_select';
},
isForm() {
return this.contentType === 'form';
},
isArticle() {
return this.contentType === 'article';
},
isCSAT() {
return this.contentType === 'input_csat';
},
isIntegrations() {
return this.contentType === 'integrations';
},
},
methods: {
onResponse(messageResponse) {
this.$store.dispatch('message/update', messageResponse);
},
onOptionSelect(selectedOption) {
this.onResponse({
submittedValues: [selectedOption],
messageId: this.messageId,
});
},
onFormSubmit(formValues) {
const formValuesAsArray = Object.keys(formValues).map(key => ({
name: key,
value: formValues[key],
}));
this.onResponse({
submittedValues: formValuesAsArray,
messageId: this.messageId,
});
},
},
};
</script>
<template>
<div class="chat-bubble-wrap">
<div
v-if="
!isCards && !isOptions && !isForm && !isArticle && !isCards && !isCSAT
"
class="chat-bubble agent bg-n-background dark:bg-n-solid-3 text-n-slate-12"
>
<div
v-dompurify-html="formatMessage(message, false)"
class="message-content text-n-slate-12"
/>
<EmailInput
v-if="isTemplateEmail"
:message-id="messageId"
:message-content-attributes="messageContentAttributes"
/>
<IntegrationCard
v-if="isIntegrations"
:message-id="messageId"
:meeting-data="messageContentAttributes.data"
/>
</div>
<div v-if="isOptions">
<ChatOptions
:title="message"
:options="messageContentAttributes.items"
:hide-fields="!!messageContentAttributes.submitted_values"
@option-select="onOptionSelect"
/>
</div>
<ChatForm
v-if="isForm && !messageContentAttributes.submitted_values"
:items="messageContentAttributes.items"
:button-label="messageContentAttributes.button_label"
:submitted-values="messageContentAttributes.submitted_values"
@submit="onFormSubmit"
/>
<div v-if="isCards">
<ChatCard
v-for="item in messageContentAttributes.items"
:key="item.title"
:media-url="item.media_url"
:title="item.title"
:description="item.description"
:actions="item.actions"
/>
</div>
<div v-if="isArticle">
<ChatArticle :items="messageContentAttributes.items" />
</div>
<CustomerSatisfaction
v-if="isCSAT"
:message-content-attributes="messageContentAttributes.submitted_values"
:display-type="messageContentAttributes.display_type"
:message="message"
:message-id="messageId"
/>
</div>
</template>