Skip to content

Commit

Permalink
feat(botonic-react): create FeedbackMessage component to add thumbsUp…
Browse files Browse the repository at this point in the history
… and thumbsDown in the footer of the message
  • Loading branch information
Iru89 committed Jul 1, 2024
1 parent dc8f373 commit 3fa97b0
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 36 deletions.
3 changes: 3 additions & 0 deletions packages/botonic-react/src/assets/thumbs-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/botonic-react/src/assets/thumbs-up.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 3 additions & 19 deletions packages/botonic-react/src/components/index-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ErrorInfo } from 'react'
import React from 'react'

import { SENDERS } from '../index-types'
import { CoverComponentProps } from '../webchat/index-types'
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -204,21 +206,3 @@ export interface WebchatSettingsProps {
export type WrappedComponent<Props> = React.FunctionComponent<Props> & {
customTypeName: string
}

// TODO: Reuse types to be typed in respective functions
// export class ErrorBoundary<Props> extends React.Component<Props> {
// componentDidCatch(error: Error, errorInfo: ErrorInfo): void
// }

// export function createErrorBoundary<Props>(_?: {
// errorComponent: React.ComponentType
// }): ErrorBoundary<Props>

// export function customMessage<Props>(_: {
// name: string
// component: React.ComponentType<Props>
// defaultProps?: Record<string, unknown>
// errorBoundary?: ErrorBoundary<Props>
// }): WrappedComponent<Props>

// export function getDisplayName(component: React.ComponentType): string
25 changes: 17 additions & 8 deletions packages/botonic-react/src/components/message/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -36,6 +37,8 @@ export const Message = props => {
style,
imagestyle = props.imagestyle || props.imageStyle,
isUnread = true,
withfeedback,
inferenceid,
...otherProps
} = props

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -111,6 +116,8 @@ export const Message = props => {
customTypeName: decomposedChildren.customTypeName,
ack: ack,
isUnread: isUnread === 1 || isUnread === true,
withfeedback,
inferenceid,
}
addMessage(message)
}
Expand Down Expand Up @@ -255,13 +262,15 @@ export const Message = props => {
{Boolean(blob) && hasBlobTick() && getBlobTick(5)}
</BlobContainer>
</MessageContainer>
{timestampsEnabled && (
<MessageTimestamp
{timestampsEnabled || withfeedback ? (
<MessageFooter
enabletimestamps={timestampsEnabled}
messageJSON={messageJSON}
sentBy={sentBy}
style={timestampStyle}
timestamp={messageJSON.timestamp}
withfeedback={withfeedback}
inferenceid={inferenceid}
/>
)}
) : null}
</>
</ConditionalWrapper>
)
Expand Down
99 changes: 99 additions & 0 deletions packages/botonic-react/src/components/message/message-feedback.tsx
Original file line number Diff line number Diff line change
@@ -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<ButtonsState>({
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 (
<FeedbackMessageContainer>
<FeedbackButton
className={className}
disabled={disabled.positive}
onClick={() => handleClick(true)}
>
<img src={resolveImage(ThumbsUp)} />
</FeedbackButton>
<FeedbackButton
className={className}
disabled={disabled.negative}
onClick={() => handleClick(false)}
>
<img src={resolveImage(ThumbsDown)} />
</FeedbackButton>
</FeedbackMessageContainer>
)
}
52 changes: 52 additions & 0 deletions packages/botonic-react/src/components/message/message-footer.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<MessageFooterContainer
className={messageFotterClass}
isSentByUser={isSentByUser}
>
{timestampsEnabled ? (
<MessageTimestamp
sentBy={sentBy}
style={timestampStyle}
timestamp={messageJSON.timestamp}
/>
) : null}
{withfeedback ? (
<MessageFeedback inferenceid={inferenceid} messageId={messageJSON.id} />
) : null}
</MessageFooterContainer>
)
}
52 changes: 50 additions & 2 deletions packages/botonic-react/src/components/message/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,6 @@ export const TimestampContainer = styled.div<TimestampContainerProps>`
box-sizing: border-box;
width: 100%;
padding: 0px 15px 4px 15px;
padding-top: ${props => (props.isSentByUser ? '0px' : '4px')};
img {
max-width: 20px;
Expand All @@ -103,3 +101,53 @@ export const TimestampText = styled.div<TimestampTextProps>`
color: ${COLORS.SOLID_BLACK};
text-align: ${props => (props.isSentByUser ? 'right' : 'left')};
`

export const MessageFooterContainer = styled.div<TimestampContainerProps>`
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;
}
`
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -17,28 +18,27 @@ const serializeText = children => {
return text
}

const serialize = textProps => {
const serialize = (textProps: TextProps) => {
if (!textProps.markdown)
return {
text: serializeText(textProps.children),
}
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 (
<Message json={serialize(textProps)} {...textProps} type={INPUT.TEXT}>
Expand Down
8 changes: 8 additions & 0 deletions packages/botonic-react/src/webchat/tracking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export enum EventAction {
FeedbackKnowledgebase = 'feedback_knowledgebase',
}

export enum FeedbackOption {
ThumbsUp = 'thumbsUp',
ThumbsDown = 'thumbsDown',
}

0 comments on commit 3fa97b0

Please sign in to comment.