mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 03:57:52 +00:00
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>
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import { CSAT_RATINGS } from 'shared/constants/messages';
|
||||
import { CSAT_RATINGS, CSAT_DISPLAY_TYPES } from 'shared/constants/messages';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import StarRating from 'shared/components/StarRating.vue';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Spinner,
|
||||
FluentIcon,
|
||||
StarRating,
|
||||
},
|
||||
props: {
|
||||
messageContentAttributes: {
|
||||
@@ -19,6 +21,14 @@ export default {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
displayType: {
|
||||
type: String,
|
||||
default: CSAT_DISPLAY_TYPES.EMOJI,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -47,7 +57,13 @@ export default {
|
||||
title() {
|
||||
return this.isRatingSubmitted
|
||||
? this.$t('CSAT.SUBMITTED_TITLE')
|
||||
: this.$t('CSAT.TITLE');
|
||||
: this.message || this.$t('CSAT.TITLE');
|
||||
},
|
||||
isEmojiType() {
|
||||
return this.displayType === CSAT_DISPLAY_TYPES.EMOJI;
|
||||
},
|
||||
isStarType() {
|
||||
return this.displayType === CSAT_DISPLAY_TYPES.STAR;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -88,10 +104,15 @@ export default {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
},
|
||||
|
||||
selectRating(rating) {
|
||||
this.selectedRating = rating.value;
|
||||
this.onSubmit();
|
||||
},
|
||||
selectStarRating(value) {
|
||||
this.selectedRating = value;
|
||||
this.onSubmit();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -104,7 +125,7 @@ export default {
|
||||
<h6 class="text-n-slate-12 text-sm font-medium pt-5 px-2.5 text-center">
|
||||
{{ title }}
|
||||
</h6>
|
||||
<div class="ratings flex justify-around py-5 px-4">
|
||||
<div v-if="isEmojiType" class="ratings flex justify-around py-5 px-4">
|
||||
<button
|
||||
v-for="rating in ratings"
|
||||
:key="rating.key"
|
||||
@@ -114,6 +135,12 @@ export default {
|
||||
{{ rating.emoji }}
|
||||
</button>
|
||||
</div>
|
||||
<StarRating
|
||||
v-else-if="isStarType"
|
||||
:selected-rating="selectedRating"
|
||||
:is-disabled="isRatingSubmitted"
|
||||
@select-rating="selectStarRating"
|
||||
/>
|
||||
<form
|
||||
v-if="!isFeedbackSubmitted"
|
||||
class="feedback-form flex"
|
||||
|
||||
65
app/javascript/shared/components/StarRating.vue
Normal file
65
app/javascript/shared/components/StarRating.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
selectedRating: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
isDisabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['selectRating']);
|
||||
|
||||
const starRatings = [1, 2, 3, 4, 5];
|
||||
const hoveredRating = ref(0);
|
||||
|
||||
const onHoverRating = value => {
|
||||
if (props.isDisabled) return;
|
||||
hoveredRating.value = value;
|
||||
};
|
||||
|
||||
const selectRating = value => {
|
||||
if (props.isDisabled) return;
|
||||
emit('selectRating', value);
|
||||
};
|
||||
|
||||
const getStarClass = value => {
|
||||
const isStarActive =
|
||||
(hoveredRating.value > 0 &&
|
||||
!props.isDisabled &&
|
||||
hoveredRating.value >= value) ||
|
||||
props.selectedRating >= value;
|
||||
|
||||
const starTypeClass = isStarActive
|
||||
? 'i-ri-star-fill text-n-amber-9'
|
||||
: 'i-ri-star-line text-n-slate-10';
|
||||
|
||||
return starTypeClass;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-center py-5 px-4 gap-3">
|
||||
<button
|
||||
v-for="value in starRatings"
|
||||
:key="value"
|
||||
type="button"
|
||||
class="rounded-full p-1 transition-all duration-200 focus:enabled:scale-[1.2] focus-within:enabled:scale-[1.2] hover:enabled:scale-[1.2] focus:outline-none flex items-center flex-shrink-0"
|
||||
:class="{ 'cursor-not-allowed opacity-50': isDisabled }"
|
||||
:disabled="isDisabled"
|
||||
:aria-label="'Star ' + value"
|
||||
@click="selectRating(value)"
|
||||
@mouseenter="onHoverRating(value)"
|
||||
@mouseleave="onHoverRating(0)"
|
||||
>
|
||||
<span
|
||||
:class="getStarClass(value)"
|
||||
class="transition-all duration-500 text-2xl"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -100,6 +100,11 @@ export const CSAT_RATINGS = [
|
||||
},
|
||||
];
|
||||
|
||||
export const CSAT_DISPLAY_TYPES = {
|
||||
EMOJI: 'emoji',
|
||||
STAR: 'star',
|
||||
};
|
||||
|
||||
export const AUDIO_FORMATS = {
|
||||
WEBM: 'audio/webm',
|
||||
OGG: 'audio/ogg',
|
||||
|
||||
Reference in New Issue
Block a user