Skip to content

Commit

Permalink
Merge pull request #98 from RasaHQ/rating-2.0
Browse files Browse the repository at this point in the history
Add Rating component v2
  • Loading branch information
Amina-Derouiche authored Feb 10, 2025
2 parents cb2dff6 + 5b56d60 commit 6e52d0c
Show file tree
Hide file tree
Showing 15 changed files with 243 additions and 117 deletions.
7 changes: 4 additions & 3 deletions e2e/cypress/fixtures/chatbotWidgetData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,11 @@ const botResponses = {
type: 'rating',
text: 'How would you rate this answer?',
options: [
{ value: 'positive', icon: '😊', label: 'Positive' },
{ value: 'neutral', icon: '😐', label: 'Neutral' },
{ value: 'negative', icon: '☹️', label: 'Negative' },
{ value: 'positive', payload: '/give_positive_feedback' },
{ value: 'neutral', payload: '/give_neutral_feedback' },
{ value: 'negative', payload: '/give_negative_feedback' },
],
message: 'We appreciate your feedback!',
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,12 @@ export interface AccordionMessage extends BaseMessage {
export interface RatingMessage extends BaseMessage {
type: typeof MESSAGE_TYPES.RATING;
text: string;
options: { value: string; icon: string; label: string }[];
options: { value: string; payload: string }[];
message: string;
}



export type Message =
| AccordionMessage
| CarouselMessage
Expand Down
18 changes: 11 additions & 7 deletions packages/sdk/src/message-parser/utils/message-parsers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,22 +185,26 @@ describe('MessageParsers', () => {
type: RESPONSE_MESSAGE_TYPES.RATING,
text: 'How would you rate this?',
options: [
{ value: 'positive', icon: '😊', label: 'Positive' },
{ value: 'neutral', icon: '😐', label: 'Neutral' },
{ value: 'negative', icon: '☹️', label: 'Negative' },
{ value: 'positive', payload: '/give_positive_feedback' },
{ value: 'neutral', payload: '/give_neutral_feedback' },
{ value: 'negative', payload: '/give_negative_feedback' },
],
message: 'We appreciate your feedback!',
};

const expected: RatingMessage = {
sender,
type: MESSAGE_TYPES.RATING,
text: 'How would you rate this?',
options: [
{ value: 'positive', icon: '😊', label: 'Positive' },
{ value: 'neutral', icon: '😐', label: 'Neutral' },
{ value: 'negative', icon: '☹️', label: 'Negative' },
{ value: 'positive', payload: '/give_positive_feedback' },
{ value: 'neutral', payload: '/give_neutral_feedback' },
{ value: 'negative', payload: '/give_negative_feedback' },
],
message: 'We appreciate your feedback!',
};

expect(MessageParsers.rating(ratingResponse, sender)).toEqual(expected);
});

});
7 changes: 6 additions & 1 deletion packages/sdk/src/message-parser/utils/message-parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,14 @@ export const MessageParsers = {
sender,
type: MESSAGE_TYPES.RATING,
text: message.text,
options: message.options,
options: message.options.map(option => ({
value: option.value,
payload: option.payload
})),
message: message.message,
timestamp: message.timestamp,
}),

};

export type MessageParsersType = typeof MessageParsers;
Expand Down
3 changes: 2 additions & 1 deletion packages/sdk/src/types/server-response.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export interface CarouselResponse extends BaseMessageResponse {
export interface RatingResponse extends BaseMessageResponse {
type: typeof RESPONSE_MESSAGE_TYPES.RATING;
text: string;
options: { value: string; icon: string; label: string }[];
options: { value: string; payload: string }[];
message: string;
}

export interface QuickReplyResponse extends BaseMessageResponse {
Expand Down
20 changes: 14 additions & 6 deletions packages/ui/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,9 +424,13 @@ export namespace Components {
}
interface RasaRating {
/**
* List of rating options
* Customizable message from Rasa (Previously thankYouMessage)
*/
"options": string | { value: string; icon: string; label: string }[];
"message": string;
/**
* List of rating options from Rasa
*/
"options": string | { value: string; payload: string }[];
/**
* Instructional text for the rating component
*/
Expand Down Expand Up @@ -737,7 +741,7 @@ declare global {
new (): HTMLRasaQuickReplyElement;
};
interface HTMLRasaRatingElementEventMap {
"ratingSelected": { value: string };
"ratingSelected": { value: string; payload: string };
}
interface HTMLRasaRatingElement extends Components.RasaRating, HTMLStencilElement {
addEventListener<K extends keyof HTMLRasaRatingElementEventMap>(type: K, listener: (this: HTMLRasaRatingElement, ev: RasaRatingCustomEvent<HTMLRasaRatingElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
Expand Down Expand Up @@ -1295,14 +1299,18 @@ declare namespace LocalJSX {
"quickReplyId"?: string;
}
interface RasaRating {
/**
* Customizable message from Rasa (Previously thankYouMessage)
*/
"message"?: string;
/**
* Event emitted when a rating is selected
*/
"onRatingSelected"?: (event: RasaRatingCustomEvent<{ value: string }>) => void;
"onRatingSelected"?: (event: RasaRatingCustomEvent<{ value: string; payload: string }>) => void;
/**
* List of rating options
* List of rating options from Rasa
*/
"options"?: string | { value: string; icon: string; label: string }[];
"options"?: string | { value: string; payload: string }[];
/**
* Instructional text for the rating component
*/
Expand Down
23 changes: 23 additions & 0 deletions packages/ui/src/components/rating/icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const ratingIcons = {
positive: `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="white" stroke="#4CAF50" stroke-width="2"/>
<circle cx="9" cy="10" r="1.5" fill="#4CAF50"/>
<circle cx="15" cy="10" r="1.5" fill="#4CAF50"/>
<path d="M8.5 14.5c1.75 2 5.25 2 7 0" stroke="#4CAF50" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`,

neutral: `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="white" stroke="#FFEB3B" stroke-width="2"/>
<circle cx="9" cy="10" r="1.5" fill="#FFEB3B"/>
<circle cx="15" cy="10" r="1.5" fill="#FFEB3B"/>
<path d="M8.5 14h7" stroke="#FFEB3B" stroke-width="2" stroke-linecap="round"/>
</svg>`,

negative: `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="white" stroke="#F44336" stroke-width="2"/>
<circle cx="9" cy="10" r="1.5" fill="#F44336"/>
<circle cx="15" cy="10" r="1.5" fill="#F44336"/>
<path d="M8.5 15.5c1.75-2 5.25-2 7 0" stroke="#F44336" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`
};

79 changes: 60 additions & 19 deletions packages/ui/src/components/rating/rating.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,70 +2,111 @@
border-radius: 12px;
background-color: var(--rating-background-color, #fff);
color: var(--rating-text-color, #333);
margin: 0;
padding: 8px 16px 12px;
margin: 0 auto;
padding: 8px;
text-align: center;
max-width: 400px;

&__text {
margin: 0 0 12px;
font-size: 1rem;
font-weight: 200;
font-weight: 500;
color: var(--rating-instruction-color, #333);
text-align: left;
width: 100%;
display: block;
}

&__thank-you {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--rating-thank-you-color, #4CAF50);
text-align: center;
animation: fadeIn 0.3s ease-in-out;
}

&__options {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 12px;
justify-content: space-between;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
}

&__option {
background: var(--rating-option-background-color, #f9f9f9);
width: 60px;
height: 60px;
background: var(--rating-option-background-color, #fff);
border: 1px solid var(--rating-option-border-color, #ddd);
border-radius: 8px;
padding: 8px 12px;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: transform 0.2s ease, background-color 0.3s, box-shadow 0.3s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
flex: none;

&:hover {
background: var(--rating-option-hover-color, #f0f0f0);
transform: translateY(-1px) scale(1.02);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15);
background: var(--rating-option-hover-color, #f9f9f9);
transform: scale(1.05);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
}

&--selected {
border-color: var(--rating-option-selected-border-color, #007bff);
background: var(--rating-option-selected-background-color, #e9f5ff);
color: var(--rating-option-selected-text-color, #007bff);
box-shadow: 0 4px 6px rgba(0, 123, 255, 0.2);
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.2);
}
}

&__icon {
font-size: 1.25rem;
margin-bottom: 4px;
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;

svg {
width: 100%;
height: 100%;
display: block;
}
}

&__label {
font-size: 0.875rem;
font-weight: 500;
color: var(--rating-option-label-color, #444);
text-align: center;
display: block;
}
}

@media (max-width: 768px) {
.rasa-rating__options {
gap: 8px;
gap: 6px;
}

.rasa-rating__option {
flex: 1 1 calc(33% - 16px);
max-width: 100px;
padding: 6px 10px;
width: 50px;
height: 50px;
padding: 6px;
}

.rasa-rating__thank-you {
font-size: 1rem;
}
}

@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
72 changes: 34 additions & 38 deletions packages/ui/src/components/rating/rating.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,21 @@ describe('rasa-rating', () => {
const page = await newSpecPage({
components: [RasaRating],
html: `<rasa-rating text="How would you rate this answer?" options='[
{ "value": "positive", "icon": "😊", "label": "Positive" },
{ "value": "neutral", "icon": "😐", "label": "Neutral" },
{ "value": "negative", "icon": "☹️", "label": "Negative" }
]'></rasa-rating>`,
{ "value": "positive", "payload": "/give_positive_feedback" },
{ "value": "neutral", "payload": "/give_neutral_feedback" },
{ "value": "negative", "payload": "/give_negative_feedback" }
]' message="We appreciate your feedback!"></rasa-rating>`,
});

expect(page.root.shadowRoot).toEqualHtml(`
<div class="rasa-rating">
<p class="rasa-rating__text">How would you rate this answer?</p>
<div class="rasa-rating__options">
<button class="rasa-rating__option">
<span class="rasa-rating__icon">😊</span>
<span class="rasa-rating__label">Positive</span>
</button>
<button class="rasa-rating__option">
<span class="rasa-rating__icon">😐</span>
<span class="rasa-rating__label">Neutral</span>
</button>
<button class="rasa-rating__option">
<span class="rasa-rating__icon">☹️</span>
<span class="rasa-rating__label">Negative</span>
</button>
</div>
</div>
`);
expect(page.root.shadowRoot.innerHTML).toContain('How would you rate this answer?');
expect(page.root.shadowRoot.querySelectorAll('.rasa-rating__option').length).toBe(3);
});

it('emits ratingSelected event on option click', async () => {
it('emits ratingSelected event with payload on option click', async () => {
const page = await newSpecPage({
components: [RasaRating],
html: `<rasa-rating text="Rate the answer" options='[
{ "value": "positive", "icon": "😊", "label": "Positive" }
{ "value": "positive", "payload": "/give_positive_feedback" }
]'></rasa-rating>`,
});

Expand All @@ -49,7 +32,10 @@ describe('rasa-rating', () => {
await page.waitForChanges();

expect(ratingSelectedSpy).toHaveBeenCalled();
expect(ratingSelectedSpy.mock.calls[0][0].detail).toEqual({ value: 'positive' });
expect(ratingSelectedSpy.mock.calls[0][0].detail).toEqual({
value: 'positive',
payload: '/give_positive_feedback'
});
});

it('renders fallback when no options are provided', async () => {
Expand All @@ -62,24 +48,34 @@ describe('rasa-rating', () => {
expect(page.root.shadowRoot.textContent).toContain('Rate the answer');
});

it('applies the selected class on click', async () => {
it('displays message after selection', async () => {
const page = await newSpecPage({
components: [RasaRating],
html: `<rasa-rating text="Rate the answer" options='[
{ "value": "positive", "icon": "😊", "label": "Positive" },
{ "value": "neutral", "icon": "😐", "label": "Neutral" }
]'></rasa-rating>`,
html: `<rasa-rating text="How would you rate this answer?" options='[
{ "value": "positive", "payload": "/give_positive_feedback" }
]' message="We appreciate your feedback!"></rasa-rating>`,
});

const options = page.root.shadowRoot.querySelectorAll('.rasa-rating__option');
const positiveOption = options[0];
const neutralOption = options[1];
const option = page.root.shadowRoot.querySelector('.rasa-rating__option');
option.dispatchEvent(new Event('click'));
await page.waitForChanges();

// Click the positive option
positiveOption.dispatchEvent(new Event('click'));
expect(page.root.shadowRoot.querySelector('.rasa-rating__options')).toBeNull(); // Ensure options disappear
expect(page.root.shadowRoot.textContent).toContain("We appreciate your feedback!"); // Ensure message is displayed
});

it('displays default message if none is provided', async () => {
const page = await newSpecPage({
components: [RasaRating],
html: `<rasa-rating text="Rate this answer" options='[
{ "value": "positive", "payload": "/give_positive_feedback" }
]'></rasa-rating>`,
});

const option = page.root.shadowRoot.querySelector('.rasa-rating__option');
option.dispatchEvent(new Event('click'));
await page.waitForChanges();

expect(positiveOption.classList.contains('rasa-rating__option--selected')).toBe(true);
expect(neutralOption.classList.contains('rasa-rating__option--selected')).toBe(false);
expect(page.root.shadowRoot.textContent).toContain("Thank you for your feedback!"); // Default message should appear
});
});
Loading

0 comments on commit 6e52d0c

Please sign in to comment.