From 3fa97b0398873da8a084239a7a54696fdf0142e2 Mon Sep 17 00:00:00 2001 From: Oriol Raventos Date: Fri, 21 Jun 2024 13:32:12 +0200 Subject: [PATCH] feat(botonic-react): create FeedbackMessage component to add thumbsUp and thumbsDown in the footer of the message --- .../botonic-react/src/assets/thumbs-down.svg | 3 + .../botonic-react/src/assets/thumbs-up.svg | 3 + .../src/components/index-types.ts | 22 +---- .../src/components/message/index.jsx | 25 +++-- .../components/message/message-feedback.tsx | 99 +++++++++++++++++++ .../src/components/message/message-footer.tsx | 52 ++++++++++ .../src/components/message/styles.ts | 52 +++++++++- .../src/components/{text.jsx => text.tsx} | 14 +-- .../botonic-react/src/webchat/tracking.ts | 8 ++ 9 files changed, 242 insertions(+), 36 deletions(-) create mode 100644 packages/botonic-react/src/assets/thumbs-down.svg create mode 100644 packages/botonic-react/src/assets/thumbs-up.svg create mode 100644 packages/botonic-react/src/components/message/message-feedback.tsx create mode 100644 packages/botonic-react/src/components/message/message-footer.tsx rename packages/botonic-react/src/components/{text.jsx => text.tsx} (85%) create mode 100644 packages/botonic-react/src/webchat/tracking.ts diff --git a/packages/botonic-react/src/assets/thumbs-down.svg b/packages/botonic-react/src/assets/thumbs-down.svg new file mode 100644 index 0000000000..2d967cb3b8 --- /dev/null +++ b/packages/botonic-react/src/assets/thumbs-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/botonic-react/src/assets/thumbs-up.svg b/packages/botonic-react/src/assets/thumbs-up.svg new file mode 100644 index 0000000000..4885323b86 --- /dev/null +++ b/packages/botonic-react/src/assets/thumbs-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/botonic-react/src/components/index-types.ts b/packages/botonic-react/src/components/index-types.ts index e70fc4c50c..0bdf603f60 100644 --- a/packages/botonic-react/src/components/index-types.ts +++ b/packages/botonic-react/src/components/index-types.ts @@ -1,4 +1,4 @@ -import React, { ErrorInfo } from 'react' +import React from 'react' import { SENDERS } from '../index-types' import { CoverComponentProps } from '../webchat/index-types' @@ -29,6 +29,8 @@ export interface MessageProps { export interface TextProps extends MessageProps { // converts markdown syntax to HTML markdown?: boolean + withfeedback?: boolean + inferenceid?: string } export interface Webview { @@ -204,21 +206,3 @@ export interface WebchatSettingsProps { export type WrappedComponent = React.FunctionComponent & { customTypeName: string } - -// TODO: Reuse types to be typed in respective functions -// export class ErrorBoundary extends React.Component { -// componentDidCatch(error: Error, errorInfo: ErrorInfo): void -// } - -// export function createErrorBoundary(_?: { -// errorComponent: React.ComponentType -// }): ErrorBoundary - -// export function customMessage(_: { -// name: string -// component: React.ComponentType -// defaultProps?: Record -// errorBoundary?: ErrorBoundary -// }): WrappedComponent - -// export function getDisplayName(component: React.ComponentType): string diff --git a/packages/botonic-react/src/components/message/index.jsx b/packages/botonic-react/src/components/message/index.jsx index 9017c740ae..14cd7f9c5b 100644 --- a/packages/botonic-react/src/components/message/index.jsx +++ b/packages/botonic-react/src/components/message/index.jsx @@ -12,6 +12,7 @@ import { Button } from '../button' import { ButtonsDisabler } from '../buttons-disabler' import { getMarkdownStyle, renderLinks, renderMarkdown } from '../markdown' import { Reply } from '../reply' +import { MessageFooter } from './message-footer' import { MessageImage } from './message-image' import { BlobContainer, @@ -20,7 +21,7 @@ import { BlobTickContainer, MessageContainer, } from './styles' -import { MessageTimestamp, resolveMessageTimestamps } from './timestamps' +import { resolveMessageTimestamps } from './timestamps' export const Message = props => { const { defaultTyping, defaultDelay } = useContext(RequestContext) @@ -36,6 +37,8 @@ export const Message = props => { style, imagestyle = props.imagestyle || props.imageStyle, isUnread = true, + withfeedback, + inferenceid, ...otherProps } = props @@ -67,8 +70,10 @@ export const Message = props => { typeof e === 'string' ? renderLinks(e) : e ) - const { timestampsEnabled, getFormattedTimestamp, timestampStyle } = - resolveMessageTimestamps(getThemeProperty, enabletimestamps) + const { timestampsEnabled, getFormattedTimestamp } = resolveMessageTimestamps( + getThemeProperty, + enabletimestamps + ) const getEnvAck = () => { if (isDev) return 1 @@ -111,6 +116,8 @@ export const Message = props => { customTypeName: decomposedChildren.customTypeName, ack: ack, isUnread: isUnread === 1 || isUnread === true, + withfeedback, + inferenceid, } addMessage(message) } @@ -255,13 +262,15 @@ export const Message = props => { {Boolean(blob) && hasBlobTick() && getBlobTick(5)} - {timestampsEnabled && ( - - )} + ) : null} ) diff --git a/packages/botonic-react/src/components/message/message-feedback.tsx b/packages/botonic-react/src/components/message/message-feedback.tsx new file mode 100644 index 0000000000..20964877df --- /dev/null +++ b/packages/botonic-react/src/components/message/message-feedback.tsx @@ -0,0 +1,99 @@ +import React, { useContext, useEffect, useState } from 'react' +import { v4 as uuid } from 'uuid' + +import ThumbsDown from '../../assets/thumbs-down.svg' +import ThumbsUp from '../../assets/thumbs-up.svg' +import { RequestContext, WebchatContext } from '../../contexts' +import { ActionRequest } from '../../index-types' +import { resolveImage } from '../../util' +import { EventAction, FeedbackOption } from '../../webchat/tracking' +import { FeedbackButton, FeedbackMessageContainer } from './styles' + +interface ButtonsState { + positive: boolean + negative: boolean +} + +interface RatingProps { + inferenceid?: string + messageId: string +} + +export const MessageFeedback = ({ inferenceid, messageId }: RatingProps) => { + const { webchatState, updateMessage, trackEvent } = useContext(WebchatContext) + const request = useContext(RequestContext) + + const [className, setClassName] = useState('') + const [disabled, setDisabled] = useState({ + positive: false, + negative: false, + }) + + const updateMsgWithFeedback = (withfeedback: boolean) => { + const message = webchatState.messagesJSON.find( + message => message.id === messageId + ) + const updatedMsg = { + ...message, + withfeedback, + } + updateMessage(updatedMsg) + } + + useEffect(() => { + updateMsgWithFeedback(true) + }, []) + + useEffect(() => { + if (disabled.positive || disabled.negative) { + setClassName('clicked') + updateMsgWithFeedback(false) + } + }, [disabled]) + + const handleClick = async (isUseful: boolean) => { + if (!trackEvent) { + return + } + + if (isUseful) { + setDisabled({ positive: false, negative: true }) + } else { + setDisabled({ positive: true, negative: false }) + } + + const args = { + knowledgebaseInferenceId: inferenceid, + feedbackTargetId: messageId, + feedbackGroupId: uuid(), + possibleOptions: [FeedbackOption.ThumbsUp, FeedbackOption.ThumbsDown], + possibleValues: [0, 1], + option: isUseful ? FeedbackOption.ThumbsUp : FeedbackOption.ThumbsDown, + value: isUseful ? 1 : 0, + } + await trackEvent( + request as ActionRequest, + EventAction.FeedbackKnowledgebase, + args + ) + } + + return ( + + handleClick(true)} + > + + + handleClick(false)} + > + + + + ) +} diff --git a/packages/botonic-react/src/components/message/message-footer.tsx b/packages/botonic-react/src/components/message/message-footer.tsx new file mode 100644 index 0000000000..d61f0192f8 --- /dev/null +++ b/packages/botonic-react/src/components/message/message-footer.tsx @@ -0,0 +1,52 @@ +import React, { useContext } from 'react' + +import { WebchatContext } from '../../contexts' +import { SENDERS } from '../../index-types' +import { MessageFeedback } from './message-feedback' +import { MessageFooterContainer } from './styles' +import { MessageTimestamp, resolveMessageTimestamps } from './timestamps' + +interface MessageFooterProps { + enabletimestamps: boolean + messageJSON: any + sentBy: SENDERS + withfeedback: boolean + inferenceid?: string +} + +export const MessageFooter = ({ + enabletimestamps, + messageJSON, + sentBy, + withfeedback, + inferenceid, +}: MessageFooterProps) => { + const { getThemeProperty } = useContext(WebchatContext) + + const { timestampsEnabled, timestampStyle } = resolveMessageTimestamps( + getThemeProperty, + enabletimestamps + ) + const isSentByUser = sentBy === SENDERS.user + const messageFotterClass = isSentByUser + ? 'message-footer-user' + : 'message-footer-bot' + + return ( + + {timestampsEnabled ? ( + + ) : null} + {withfeedback ? ( + + ) : null} + + ) +} diff --git a/packages/botonic-react/src/components/message/styles.ts b/packages/botonic-react/src/components/message/styles.ts index 5b8d26597a..71bf0d88e1 100644 --- a/packages/botonic-react/src/components/message/styles.ts +++ b/packages/botonic-react/src/components/message/styles.ts @@ -85,8 +85,6 @@ export const TimestampContainer = styled.div` box-sizing: border-box; width: 100%; - padding: 0px 15px 4px 15px; - padding-top: ${props => (props.isSentByUser ? '0px' : '4px')}; img { max-width: 20px; @@ -103,3 +101,53 @@ export const TimestampText = styled.div` color: ${COLORS.SOLID_BLACK}; text-align: ${props => (props.isSentByUser ? 'right' : 'left')}; ` + +export const MessageFooterContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + + box-sizing: border-box; + padding: 0px 15px 4px 15px; + padding-top: ${props => (props.isSentByUser ? '0px' : '4px')}; + width: 100%; +` + +export const FeedbackMessageContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + gap: 4px; + + box-sizing: border-box; +` + +export const FeedbackButton = styled.button` + display: flex; + justify-content: center; + align-items: center; + + background: none; + box-sizing: border-box; + border: none; + border-radius: 4px; + padding: 8px 8px; + height: 24px; + width: 24px; + + &:hover { + cursor: pointer; + background-color: #f4f3f4; + } + + &:disabled { + cursor: default; + background: none; + opacity: 0.3; + } + + &.clicked { + opacity: 0; + transition: 1s 1s; + } +` diff --git a/packages/botonic-react/src/components/text.jsx b/packages/botonic-react/src/components/text.tsx similarity index 85% rename from packages/botonic-react/src/components/text.jsx rename to packages/botonic-react/src/components/text.tsx index 58b0d9d1dc..f4a9cbf89c 100644 --- a/packages/botonic-react/src/components/text.jsx +++ b/packages/botonic-react/src/components/text.tsx @@ -2,6 +2,7 @@ import { INPUT } from '@botonic/core' import React, { Children } from 'react' import { mapObjectNonBooleanValues } from '../util/react' +import { TextProps } from './index-types' import { serializeMarkdown, toMarkdownChildren } from './markdown' import { Message } from './message' @@ -17,7 +18,7 @@ const serializeText = children => { return text } -const serialize = textProps => { +const serialize = (textProps: TextProps) => { if (!textProps.markdown) return { text: serializeText(textProps.children), @@ -25,20 +26,19 @@ const serialize = textProps => { return { text: serializeMarkdown(textProps.children) } } -/** - * - * @param {TextProps} props - * @returns {JSX.Element} - */ -export const Text = props => { +export const Text = (props: TextProps) => { const defaultTextProps = { markdown: props.markdown === undefined ? true : props.markdown, + withfeedback: props.withfeedback, + inferenceid: props.inferenceid, } + const textProps = mapObjectNonBooleanValues({ ...props, ...defaultTextProps, ...{ children: Children.toArray(props.children) }, }) + if (!textProps.markdown) return ( diff --git a/packages/botonic-react/src/webchat/tracking.ts b/packages/botonic-react/src/webchat/tracking.ts new file mode 100644 index 0000000000..c30603d403 --- /dev/null +++ b/packages/botonic-react/src/webchat/tracking.ts @@ -0,0 +1,8 @@ +export enum EventAction { + FeedbackKnowledgebase = 'feedback_knowledgebase', +} + +export enum FeedbackOption { + ThumbsUp = 'thumbsUp', + ThumbsDown = 'thumbsDown', +}